PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / trunk
Fluent Support – Helpdesk & Customer Support Ticket System vtrunk
2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 All 68 releases
fluent-support / vendor / wpfluent / framework / src / WPFluent / Http / Route.php

Route.php in Fluent Support – Helpdesk & Customer Support Ticket System trunk, at vendor/wpfluent/framework/src/WPFluent/Http/Route.php

1,683 lines 40.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentSupport\Framework\Http;
4
5 use Closure;
6 use Exception;
7 use Throwable;
8 use WP_Error;
9 use WP_REST_Request;
10 use WP_REST_Response;
11 use ReflectionClass;
12 use BadMethodCallException;
13 use InvalidArgumentException;
14 use FluentSupport\Framework\Support\Arr;
15 use FluentSupport\Framework\Support\Str;
16 use FluentSupport\Framework\Support\Pipeline;
17 use FluentSupport\Framework\Http\Request\Request;
18 use FluentSupport\Framework\Http\Request\WPUserProxy;
19 use FluentSupport\Framework\Http\SubstituteParameters;
20 use FluentSupport\Framework\Http\Middleware\RateLimiter;
21 use FluentSupport\Framework\Validator\ValidationException;
22 use FluentSupport\Framework\Database\Orm\ModelNotFoundException;
23 use FluentSupport\Framework\Http\Response\Response as WPFluentResponse;
24
25 class Route
26 {
27 use SubstituteParameters;
28
29 /**
30 * Application Instance
31 * @var \FluentSupport\Framework\Foundation\Application
32 */
33 protected $app = null;
34
35 /**
36 * Route name
37 * @var string
38 */
39 protected $name = null;
40
41 /**
42 * Rest namespace from config
43 * @var string
44 */
45 protected $restNamespace = null;
46
47 /**
48 * Whether this route should override existing routes at the same URI.
49 * @var bool
50 */
51 protected $shouldOverride = false;
52
53 /**
54 * Full URI
55 * @var string
56 */
57 protected $uri = null;
58
59 /**
60 * Compiled rest endpoint
61 * @var string
62 */
63 protected $compiled = null;
64
65 /**
66 * Route meta data
67 * @var array
68 */
69 protected $meta = [];
70
71 /**
72 * Rest Handler/Callback before parsing
73 * @var string
74 */
75 protected $handler = null;
76
77 /**
78 * Rest Handler/Callback after parsing
79 * @var callable|string
80 */
81 protected $action = null;
82
83 /**
84 * Rest route action info after parsing
85 * @var array
86 */
87 protected $actionInfo = [];
88
89 /**
90 * Policy Handler/Callback after parsing
91 * @var string
92 */
93 protected $permissionHandler = [];
94
95 /**
96 * HTTP Methods
97 * @var string
98 */
99 protected $method = null;
100
101 /**
102 * Rest options
103 * @var array
104 */
105 protected $options = [];
106
107 /**
108 * Route where constraints
109 * @var array
110 */
111 protected $wheres = [];
112
113 /**
114 * Rest namespace
115 * @var string
116 */
117 protected $namespace = null;
118
119 /**
120 * Policy Handler/Callback after parsing
121 * @var callable|string
122 */
123 protected $policyHandler = null;
124
125 /**
126 * Route Middleware
127 * @var array
128 */
129 protected $middleware = [
130 'before' => [],
131 'after' => []
132 ];
133
134 /**
135 * Skips middlewar if true
136 *
137 * @var boolean
138 */
139 protected $skipMiddleware = false;
140
141 /**
142 * Predefined Regex foe where constraints
143 * @var array
144 */
145 protected $predefinedNamedRegx = [
146 'int' => '[0-9]+',
147 'alpha' => '[a-zA-Z]+',
148 'alpha_num' => '[a-zA-Z0-9]+',
149 'alpha_num_dash' => '[a-zA-Z0-9-_]+'
150 ];
151
152 /**
153 * Route parameters
154 * @var null|array
155 */
156 protected $parameters = null;
157
158 /**
159 * Route substituted parameters
160 *
161 * @var null|array
162 */
163 protected $substitutedParameters = [];
164
165 /**
166 * Is route signed
167 *
168 * @var boolean
169 */
170 protected $signed = false;
171
172 /**
173 * Route signature.
174 *
175 * @var array
176 */
177 protected $endpointSignature = [];
178
179 /**
180 * Response instance
181 *
182 * @var \WP_REST_Response
183 */
184 protected $response = null;
185
186 /**
187 * Construct the route instance
188 *
189 * @param \FluentSupport\Framework\Foundation\Application $app
190 * @param string $restNamespace
191 * @param string $uri
192 * @param string $handler
193 * @param string $method
194 */
195 public function __construct($app, $restNamespace, $uri, $handler, $method)
196 {
197 $this->app = $app;
198 $this->restNamespace = $restNamespace;
199 $this->uri = $uri;
200 $this->handler = $handler;
201 $this->method = $method;
202 }
203
204 /**
205 * Map the route to be used in front-end.
206 *
207 * @param mixed $handler
208 * @return self
209 */
210 public function preparefrontendHandlers()
211 {
212 $handler = $this->handler;
213
214 $endpointsUrl = $this->app->config->get('app.slug') . '/__endpoints';
215
216 if (get_option('permalink_structure')) {
217 $url = $this->app->request->url();
218 } else {
219 $url = $this->app->request->query('rest_route');
220 }
221
222 if (
223 !str_contains($url ?? '', $endpointsUrl)
224 || $handler instanceof Closure
225 ) {
226 return $this;
227 }
228
229 [$controller, $cb] = Str::parseCallback($this->parseAction($handler));
230
231 $this->endpointSignature = [$controller, "_{$cb}"];
232
233 $controller = str_replace('\\', '.', $controller);
234
235 // @phpstan-ignore-next-line
236 $endpoints = $this->app->endpoints;
237
238 $endpoints[$controller]["_{$cb}"] = [
239 'uri' => $this->uri,
240 'methods' => explode(',', $this->method),
241 'policy' => $this->getPolicyName()
242 ];
243
244 // @phpstan-ignore-next-line
245 $this->app->endpoints = $endpoints;
246
247 return $this;
248 }
249
250 /**
251 * Get a display name for the route's policy handler.
252 *
253 * @return string|null
254 */
255 protected function getPolicyName()
256 {
257 if (!$this->policyHandler) {
258 return null;
259 }
260
261 if ($this->policyHandler instanceof Closure) {
262 return 'Closure';
263 }
264
265 $name = $this->policyHandler;
266
267 if (is_string($name) && !$this->app->hasNamespace($name)) {
268 $name = $this->app->__namespace__ . '\\App\\Http\\Policies\\' . $name;
269 }
270
271 return $name;
272 }
273
274 /**
275 * Parse the action from the handler.
276 *
277 * @param mixed $handler
278 * @return string
279 */
280 protected function parseAction($handler)
281 {
282 $action = $this->app->parseRestHandler($handler, $this->namespace);
283 $action = trim($action, '\\');
284
285 if (!str_contains($action, '@')) {
286 $action .= '@__invoke';
287 }
288
289 return $action;
290 }
291
292 /**
293 * Alternative constructor
294 *
295 * @param \FluentSupport\Framework\Foundation\Application $app
296 * @param string $namespace
297 * @param string $uri
298 * @param string $handler
299 * @param string $method
300 * @return self
301 */
302 public static function create($app, $namespace, $uri, $handler, $method)
303 {
304 return new static($app, $namespace, $uri, $handler, $method);
305 }
306
307 /**
308 * Set route meta
309 *
310 * @param string $key
311 * @param mixed $value
312 * @return self
313 */
314 public function meta($key, $value = null)
315 {
316 $meta = is_array($key) ? $key : [$key => $value];
317
318 $this->meta = array_merge($this->meta, $meta);
319
320 return $this;
321 }
322
323 /**
324 * Get route meta
325 *
326 * @param string $key
327 * @return mixed
328 */
329 public function getMeta($key = '')
330 {
331 if (isset($this->meta[$key])) {
332 return $this->meta[$key];
333 }
334
335 return $this->meta;
336 }
337
338 /**
339 * Get route options
340 *
341 * @return mixed
342 */
343 public function getOptions()
344 {
345 return $this->getOption();
346 }
347
348 /**
349 * Get route options
350 *
351 * @param string $key
352 * @return mixed
353 */
354 public function getOption($key = null)
355 {
356 return $key ? $this->options[$key] : $this->options;
357 }
358
359 /**
360 * Get route action information
361 * @param string $key
362 * @return mixed
363 */
364 public function getAction($key = '')
365 {
366 if ($key && array_key_exists($key, $this->actionInfo)) {
367 return $this->actionInfo[$key];
368 }
369
370 return $this->actionInfo;
371 }
372
373 /**
374 * Set a where constrain into the route
375 *
376 * @param string $identifier
377 * @param string $value
378 * @return self
379 */
380 public function where($identifier, $value = null)
381 {
382 if (!is_null($value)) {
383 $this->wheres[$identifier] = $this->getValue($value);
384 } else {
385 foreach ($identifier as $key => $value) {
386 $this->wheres[$key] = $this->getValue($value);
387 }
388 }
389
390 return $this;
391 }
392
393 /**
394 * Add an integer type route constraint
395 *
396 * @param string $identifiers
397 * @return self
398 */
399 public function int($identifiers)
400 {
401 $identifiers = is_array($identifiers) ? $identifiers : func_get_args();
402
403 foreach ($identifiers as $identifier) {
404 $this->wheres[$identifier] = '[0-9]+';
405 }
406
407 return $this;
408 }
409
410 /**
411 * Add an alpha type route constraint
412 *
413 * @param string $identifiers
414 * @return self
415 */
416 public function alpha($identifiers)
417 {
418 $identifiers = is_array($identifiers) ? $identifiers : func_get_args();
419
420 foreach ($identifiers as $identifier) {
421 $this->wheres[$identifier] = '[a-zA-Z]+';
422 }
423
424 return $this;
425 }
426
427 /**
428 * Add an alphanum type route constraint
429 *
430 * @param string $identifiers
431 * @return self
432 */
433 public function alphaNum($identifiers)
434 {
435 $identifiers = is_array($identifiers) ? $identifiers : func_get_args();
436
437 foreach ($identifiers as $identifier) {
438 $this->wheres[$identifier] = '[a-zA-Z0-9]+';
439 }
440
441 return $this;
442 }
443
444 /**
445 * Add an alphanumdash type route constraint
446 *
447 * @param string $identifiers
448 * @return self
449 */
450 public function alphaNumDash($identifiers)
451 {
452 $identifiers = is_array($identifiers) ? $identifiers : func_get_args();
453
454 foreach ($identifiers as $identifier) {
455 $this->wheres[$identifier] = '[a-zA-Z0-9-_]+';
456 }
457
458 return $this;
459 }
460
461 /**
462 * Set the route before middleware
463 *
464 * @param array|string $middleware
465 * @return self
466 */
467 public function before(...$middleware)
468 {
469 return $this->middleware('before', ...$middleware);
470 }
471
472 /**
473 * Set the route after middleware
474 *
475 * @param array|string $middleware
476 * @return self
477 */
478 public function after(...$middleware)
479 {
480 return $this->middleware('after', ...$middleware);
481 }
482
483 /**
484 * Set the route middleware
485 * @param array $middleware
486 * @return self
487 */
488 public function middleware($type = 'before', ...$middleware)
489 {
490 if (is_array($middleware[0])) {
491 $middleware = reset($middleware);
492 }
493
494 $this->middleware[$type] = array_merge(
495 $this->middleware[$type], $middleware
496 );
497
498 return $this;
499 }
500
501 /**
502 * Set the default route policy.
503 *
504 * @return self
505 */
506 public function withDefaultPolicy()
507 {
508 return $this->withPolicy(
509 // @phpstan-ignore-next-line
510 $this->app->__namespace__.'\\App\\Http\\Policies\\Policy'
511 );
512 }
513
514 /**
515 * Set the route policy
516 *
517 * @param mixed $handler
518 * @param string|null $method
519 * @return self
520 */
521 public function withPolicy($handler, $method = null)
522 {
523 if (is_array($handler = $method ? func_get_args() : $handler)) {
524 $handler = implode('@', $handler);
525 }
526
527 $this->policyHandler = $handler;
528
529 if (is_string($handler) && !$this->app->hasNamespace($handler)) {
530 $this->setPolicyHandlerWithNamespace(
531 debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4)
532 );
533 }
534
535 return $this->addRouteInfo($handler);
536 }
537
538 /**
539 * Check if the request is from CLI;
540 *
541 * @return bool
542 */
543 protected function fromCli()
544 {
545 $hash = $this->app->request->header('X-From-CLI');
546
547 $slugHash = md5($this->app->config->get('app.slug'));
548
549 return $hash === $slugHash;
550 }
551
552 /**
553 * Add route information for CLI command.
554 *
555 * @param mixed $handler
556 * @return self
557 */
558 protected function addRouteInfo($handler)
559 {
560 if (!$this->fromCli()) {
561 return $this;
562 }
563
564 if ($handler instanceof Closure) {
565 $policyHandler = 'Closure';
566 } else {
567 $policyHandler = $this->resolvePolicyHandler();
568 if (is_array($policyHandler)) {
569 $policyHandler = implode('@', $policyHandler);
570 }
571 }
572
573 $this->injectProp('policy', $policyHandler);
574
575 return $this;
576 }
577
578 /**
579 * Inject property into route infio.
580 *
581 * @param string $key
582 * @param mixed $value
583 * @return void
584 */
585 public function injectProp($key, $value)
586 {
587 if (!$this->endpointSignature) {
588 return;
589 }
590
591 [$controller, $cbKey] = $this->endpointSignature;
592
593 $controllerKey = str_replace('\\', '.', $controller);
594
595 // @phpstan-ignore-next-line
596 $endpoints = $this->app->endpoints;
597
598 if (isset($endpoints[$controllerKey][$cbKey])) {
599 $endpoints[$controllerKey][$cbKey][$key] = $value;
600 // @phpstan-ignore-next-line
601 $this->app->endpoints = $endpoints;
602 }
603 }
604
605 /**
606 * Resolve and set policy with namespace for add-ons
607 *
608 * @param array $backTrace
609 * @return void
610 */
611 protected function setPolicyHandlerWithNamespace($backTrace)
612 {
613 $last = end($backTrace);
614
615 if (!isset($last['class'])) return;
616
617 $class = $last['class'];
618
619 $namespace = substr(__NAMESPACE__, 0, strpos(__NAMESPACE__, '\\'));
620
621 $calledClassNamespace = substr($class, 0, strpos($class, '\\'));
622
623 if ($namespace != $calledClassNamespace) {
624 $ns = $calledClassNamespace . '\\App\\Http\\Policies\\';
625 $this->policyHandler = $ns . $this->policyHandler;
626 }
627 }
628
629 /**
630 * Set the name for the route.
631 *
632 * @param string $name
633 * @return self
634 */
635 public function name($name)
636 {
637 if (!$this->name) {
638 $this->name = $name;
639 } else {
640 $this->name .= $name;
641 }
642
643 // @phpstan-ignore-next-line
644 return $this->app->router->setNamedRoute($this->name, $this);
645 }
646
647 /**
648 * Set the name for the route.
649 *
650 * @param string $name
651 * @return null
652 */
653 public function withName($name)
654 {
655 $this->name = implode('', $name);
656 }
657
658 /**
659 * Set the namespace for controller/action.
660 *
661 * @param string $ns
662 * @return null
663 */
664 public function withNamespace($ns)
665 {
666 if (is_array($ns)) {
667 $this->namespace = implode('\\', $ns);
668 } else {
669 $this->namespace = trim($ns, '\\');
670 }
671 }
672
673 /**
674 * Sign the route.
675 *
676 * @return $this
677 */
678 public function signed()
679 {
680 $this->signed = true;
681
682 return $this;
683 }
684
685 /**
686 * Apply rate limit to the route.
687 *
688 * @param int $limit Number of allowed requests.
689 * @param int $interval Time interval in seconds.
690 *
691 * @return $this
692 */
693 public function rateLimit($limit, $interval)
694 {
695 // Since the rate limiter is applied twice because
696 // WordPress sends an extra request for every
697 // request, so we need to double the limit.
698
699 $rateLimiter = new RateLimiter($limit * 2, $interval);
700
701 $this->middleware('before', $rateLimiter);
702
703 return $this;
704 }
705
706 /**
707 * Apply a rate limit to the route for per minute.
708 *
709 * @param int $limit The maximum number of requests allowed per minute.
710 * @return $this
711 */
712 public function rateLimitPerMinute($limit)
713 {
714 return $this->rateLimit($limit, MINUTE_IN_SECONDS);
715 }
716
717 /**
718 * Apply an hourly rate limit to the route.
719 *
720 * @param int $limit The maximum number of requests allowed per hour.
721 * @return $this
722 */
723 public function rateLimitHourly($limit)
724 {
725 return $this->rateLimit($limit, HOUR_IN_SECONDS);
726 }
727
728 /**
729 * Apply a daily basis (24 hours) rate limit to the route.
730 *
731 * @param int $limit The maximum number of requests allowed per day.
732 * @return $this
733 */
734 public function rateLimitDaily($limit)
735 {
736 return $this->rateLimit($limit, DAY_IN_SECONDS);
737 }
738
739 /**
740 * Register the rest endpoint
741 *
742 * @return null
743 */
744 public function register()
745 {
746 $this->updateRouteOptions();
747
748 return register_rest_route(
749 $this->restNamespace,
750 $this->getRouteUri(),
751 $this->getOptions(),
752 $this->shouldOverride
753 );
754 }
755
756 /**
757 * Update route options before registering.
758 *
759 * @return void
760 */
761 protected function updateRouteOptions()
762 {
763 $this->setOptions();
764 }
765
766 /**
767 * Get normalized uri for the current route.
768 *
769 * @return string
770 */
771 protected function getRouteUri()
772 {
773 return '/' . trim($this->compileRoute($this->uri), '/');
774 }
775
776 /**
777 * Mark this route to override any existing route at the same URI.
778 *
779 * @return $this
780 */
781 public function override()
782 {
783 $this->shouldOverride = true;
784
785 return $this;
786 }
787
788 /**
789 * Set route options
790 *
791 * @return null
792 */
793 protected function setOptions()
794 {
795 $this->options = array_merge(
796 $this->options, $this->getDefaultOptions()
797 );
798 }
799
800 /**
801 * Get default options.
802 *
803 * @return array
804 */
805 protected function getDefaultOptions()
806 {
807 return [
808 [
809 'methods' => $this->method,
810 'callback' => [$this, 'callback'],
811 'permission_callback' => [$this, 'permissionCallback'],
812 'args' => [],
813 ],
814 ];
815 }
816
817 /**
818 * Generate and return the schema for the route.
819 *
820 * @return self
821 * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/schema
822 */
823 public function schema($schema)
824 {
825 $this->options['schema'] = fn() => $schema;
826
827 return $this;
828 }
829
830 /**
831 * Get item from predefined regex
832 * @param string $value
833 * @return string
834 */
835 protected function getValue($value)
836 {
837 if (array_key_exists($value, $this->predefinedNamedRegx)) {
838 return $this->predefinedNamedRegx[$value];
839 }
840
841 return $value;
842 }
843
844 /**
845 * Compikle the rest route to regex
846 *
847 * @param string $uri
848 * @return string compiled rest endpoint
849 */
850 protected function compileRoute($uri)
851 {
852 $params = [];
853
854 $compiledUri = preg_replace_callback('#/{(.*?)}#', function ($match) use (&$params, $uri) {
855 // Default regx
856 $regx = '[^\s(?!/)]+';
857
858 $param = trim($match[1]);
859
860 if ($isOptional = strpos($param, '?')) {
861 $param = trim($param, '?');
862 }
863
864 if (in_array($param, $params)) {
865 throw new InvalidArgumentException(
866 "Duplicate parameter name '{$param}' found in {$uri}.", 500
867 );
868 }
869
870 $params[] = $param;
871
872 if (isset($this->wheres[$param])) {
873 $regx = $this->wheres[$param];
874 }
875
876 $pattern = "/(?P<" . $param . ">" . $regx . ")";
877
878 if ($isOptional) {
879 $pattern = "(?:" . $pattern . ")?";
880 }
881
882 $this->options['args'][$param]['required'] = !$isOptional;
883
884 return $pattern;
885
886 }, $uri);
887
888 return $this->compiled = $compiledUri;
889 }
890
891 /**
892 * Route handler
893 *
894 * @return \WP_REST_Response
895 */
896 public function callback()
897 {
898 try {
899 $this->response = $this->handleAfterMiddleware(
900 $this->dispatchRouteAction()
901 );
902
903 return $this->handleResponse($this->response);
904
905 } catch (ValidationException $e) {
906 return $this->app->response->sendError(
907 $e->errors(), $e->getCode()
908 );
909 } catch (ModelNotFoundException $e) {
910 return $this->app->response->sendError([
911 'message' => $e->getMessage()
912 ], 404);
913 } catch (Throwable $e) {
914 return $this->handleUnknownException(
915 $e, $this->response ? $this->response->get_headers() : []
916 );
917 }
918 }
919
920 /**
921 * Handle response from route.
922 *
923 * @param \WP_REST_Response $response
924 * @return \WP_REST_Response
925 */
926 protected function handleResponse($response)
927 {
928 return $response;
929 }
930
931 /**
932 * Extract error message from response data.
933 *
934 * @param \WP_REST_Response $response
935 * @return string
936 */
937 protected function extractErrorMessage($response)
938 {
939 $data = $response->get_data();
940
941 if (is_string($data)) {
942 return $data;
943 }
944
945 if (is_array($data) && isset($data['message'])) {
946 return $data['message'];
947 }
948
949 if ($data instanceof WP_Error) {
950 return $data->get_error_message();
951 }
952
953 return 'Unknown error';
954 }
955
956 /**
957 * Throw an exception based on the status code.
958 *
959 * @param string $message
960 * @param int $status
961 * @return null
962 * @throws \Exception
963 */
964 protected function throwException($message, $status)
965 {
966 $class = sprintf(
967 'WpOrg\Requests\Exception\Http\Status%d', $status
968 );
969
970 if (!class_exists($class)) {
971 $class = 'WpOrg\Requests\Exception\Http';
972 }
973
974 throw new $class($message, $status);
975 }
976
977 /**
978 * Handle exception and send error response.
979 *
980 * @param Throwable $e
981 * @return \WP_REST_Response
982 */
983 protected function handleUnknownException(Throwable $e, $headers = [])
984 {
985 $data = [];
986
987 $this->fireExceptionEvent($e);
988
989 if ($this->app->isDebugOn()) {
990 $data = [
991 'file' => $e->getFile(),
992 'line' => $e->getLine(),
993 ];
994 }
995
996 return $this->app->response->sendError([
997 'code' => 'plugin_exception',
998 'data' => $data,
999 'message' => $e->getMessage(),
1000 ], $e->getCode() ?: 500, $headers);
1001 }
1002
1003 /**
1004 * Dispatch the route action.
1005 *
1006 * @return \WP_REST_Response
1007 */
1008 protected function dispatchRouteAction()
1009 {
1010 $response = $this->app->call(
1011 $this->action, $this->getControllerParameters()
1012 );
1013
1014 if ($response instanceof WPFluentResponse) {
1015 $response = $response->toArray();
1016 } elseif (!($response instanceof WP_REST_Response)) {
1017 $response = !is_wp_error($response) ?
1018 $this->app->response->sendSuccess($response) :
1019 $this->app->response->wpErrorToResponse($response);
1020 }
1021
1022 return $response;
1023 }
1024
1025 /**
1026 * Handle after middleware if any.
1027 *
1028 * @param mixed $response
1029 * @return mixed
1030 */
1031 protected function handleAfterMiddleware($response)
1032 {
1033 if (!$this->skipMiddleware) {
1034 $response = $this->app->make(Pipeline::class)
1035 ->send(new WPFluentResponse($response))
1036 ->through($this->collectMiddleWare('after'))
1037 ->then(function($response) {
1038 return $this->normalize($response);
1039 });
1040
1041 if (!$response) {
1042 $response = $this->app->request->abort();
1043 }
1044 }
1045
1046 return $response;
1047 }
1048
1049 /**
1050 * Normalize the response.
1051 *
1052 * @param mixed $response
1053 * @return mixed
1054 */
1055 protected function normalize($response)
1056 {
1057 if ($response instanceof WPFluentResponse) {
1058 $response = $response->toArray();
1059 }
1060
1061 if (!$response instanceof WP_REST_Response) {
1062 return new WP_REST_Response($response);
1063 }
1064
1065 return $response;
1066 }
1067
1068 /**
1069 * Fire exception action hook.
1070 *
1071 * @param Exception $exception
1072 * @return void
1073 */
1074 protected function fireExceptionEvent($exception)
1075 {
1076 if ($this->app->isDebugOn() || defined('FLUENT_BRIDGE_SECRET')) {
1077 $message = sprintf(
1078 "%s in %s:%d\nStack trace:\n%s\n",
1079 $exception->getMessage(),
1080 $exception->getFile(),
1081 $exception->getLine(),
1082 $exception->getTraceAsString()
1083 );
1084
1085 error_log($message);
1086
1087 }
1088
1089 $this->app->doAction('fluent_exception', $exception);
1090 }
1091
1092 /**
1093 * Permission callback for route
1094 * @param \WP_REST_Request $wpRestRequest
1095 * @return mixed
1096 */
1097 public function permissionCallback($wpRestRequest)
1098 {
1099 try {
1100 $this->parameters = null;
1101 $this->substitutedParameters = null;
1102 $this->app->instance('route', $this);
1103 $this->app->instance('wprestrequest', $wpRestRequest);
1104 $this->app->request->mergeInputsFromRestRequest($wpRestRequest);
1105 $this->prepareCallbacks($this->app->request);
1106
1107 if (!$this->isThisValidSignedRoute()) {
1108 throw new Exception('Invalid Signature', 403);
1109 }
1110
1111 $response = $this->app->make(Pipeline::class)
1112 ->send($this->app->request)
1113 ->through($this->collectMiddleWare('before'))
1114 ->then(function ($request) {
1115 if ($request && $request instanceof Request) {
1116 return $this->dispatchPermissionHandler();
1117 }
1118 });
1119
1120 if (is_wp_error($response)) {
1121 throw new Exception(
1122 $response->get_error_message(),
1123 is_int($code = $response->get_error_code()) ? $code : 403
1124 );
1125 }
1126
1127 if ($response instanceof WP_REST_Response) {
1128 $data = $response->get_data();
1129
1130 throw new Exception(
1131 $data['message'] ?? $response->get_status(),
1132 $response->get_status()
1133 );
1134 }
1135
1136 return $response;
1137
1138 } catch (Exception $e) {
1139 return new WP_Error(
1140 'Permission Callback Error',
1141 $e->getMessage(), [
1142 'status' => $e->getCode() ?: 403
1143 ]
1144 );
1145 }
1146 }
1147
1148 /**
1149 * Checks if the route is signed and needs validation.
1150 *
1151 * @return boolean [description]
1152 */
1153 protected function isThisValidSignedRoute()
1154 {
1155 if (!$this->signed) return true;
1156
1157 $request = $this->app->make('request');
1158
1159 if ($this->app->make('url')->validate($request->getFullUrl())) {
1160 parse_str($this->app->make('encrypter')->decrypt(
1161 $this->app->request->get('_data')
1162 ), $query);
1163
1164 $this->app->request->merge(
1165 Arr::except($query, ['expires_at'])
1166 );
1167
1168 $this->app->request->forget('_data');
1169
1170 return true;
1171 }
1172 }
1173
1174 /**
1175 * Dispatches the permission handler.
1176 *
1177 * @return bool|null
1178 */
1179 protected function dispatchPermissionHandler()
1180 {
1181 if (!$this->permissionHandler) {
1182 return true;
1183 }
1184
1185 $isValid = $this->app->call(
1186 $this->permissionHandler,
1187 $this->getControllerParameters()
1188 );
1189
1190 if (is_object($isValid)) {
1191 if ($this->isUser($isValid)) {
1192 $isValid = $isValid->id();
1193 } else {
1194 $this->throwInvalidPolicy();
1195 }
1196 }
1197
1198 if (!is_bool($isValid) && !is_int($isValid) && !is_null($isValid)) {
1199 $this->throwInvalidPolicy();
1200 }
1201
1202 return (bool) $isValid;
1203 }
1204
1205 /**
1206 * Checks if the user is an instance of WPUserProxy.
1207 *
1208 * @param WPUserProxy $user
1209 * @return bool
1210 */
1211 protected function isUser($user)
1212 {
1213 return $user instanceof WPUserProxy;
1214 }
1215
1216 /**
1217 * Throw invalid policy handling exception.
1218 *
1219 * @return InvalidArgumentException
1220 */
1221 protected function throwInvalidPolicy()
1222 {
1223 throw new InvalidArgumentException(
1224 'The policy must return a boolean, integer, null, or a WPUserProxy instance.', 500
1225 );
1226 }
1227
1228 /**
1229 * Gether route params after substituted the params
1230 *
1231 * @return array
1232 */
1233 protected function getControllerParameters()
1234 {
1235 $routeParameters = [];
1236
1237 if (!$this->substitutedParameters) {
1238 if ($routeParameters = $this->getParameter()) {
1239 $routeParameters = $this->substituteParameters($routeParameters);
1240 }
1241 } else {
1242 $routeParameters = $this->substitutedParameters;
1243 }
1244
1245 return $routeParameters;
1246 }
1247
1248 /**
1249 * Added the ability to add middleware so we can intercept
1250 * the request without modifying the source code again
1251 * and again. The middleware class will implement
1252 * the handle method as given below:
1253 *
1254 * public function handle($request, $next)
1255 *
1256 * And must return $next($request) to handle the request.
1257 * Otherwise return nothing to abort the request.
1258 * Optionally, you may call the abort method:
1259 * return $request->abort(code, message);
1260 *
1261 * @param string $type
1262 * @return array
1263 */
1264 protected function collectMiddleWare($type = 'before')
1265 {
1266 $middleware = $this->app['config']->get('middleware', []);
1267
1268 $callableMiddleware = Arr::get($middleware, "global.{$type}", []);
1269
1270 $routeArray = [];
1271
1272 if (isset($middleware['route'])) {
1273 $routeArray = $middleware['route'];
1274 if (isset($routeArray[$type])) {
1275 $routeArray = $routeArray[$type];
1276 }
1277 }
1278
1279 foreach ($this->middleware[$type] as $routeMiddleware) {
1280
1281 if (is_object($routeMiddleware)) {
1282 $handler = $routeMiddleware;
1283 } elseif (class_exists($routeMiddleware)) {
1284 $handler = $this->resolveMiddlewareFrom($routeMiddleware);
1285 } else {
1286 $pieces = explode(':', $routeMiddleware);
1287 $handler = Arr::get($routeArray, $key = reset($pieces));
1288 if (isset($pieces[1])) {
1289 $handler = $this->resolveMiddleware($handler, $pieces);
1290 }
1291 }
1292
1293 if (isset($handler)) {
1294 $this->addMiddlewareInTheStack($callableMiddleware, $handler);
1295 } else {
1296 if (isset($key)) {
1297 $mpath = 'config.middleware.route.' . $type;
1298 $msg = "No middleware is assigned for the key: {$key} in {$mpath} array.";
1299 } else {
1300 $msg = "Could't resolve middleware.";
1301 }
1302
1303 throw new InvalidArgumentException($msg);
1304 }
1305 }
1306
1307 return $callableMiddleware;
1308 }
1309
1310 /**
1311 * Resolve a middleware from a class.
1312 *
1313 * @param string $class
1314 * @return \Closure
1315 */
1316 protected function resolveMiddlewareFrom($class)
1317 {
1318 return static function ($r, $next, ...$params) use ($class) {
1319 return (new $class)->handle($r, $next, ...$params);
1320 };
1321 }
1322
1323 /**
1324 * Resolve the middleware
1325 *
1326 * @param mixed $handler
1327 * @param array $pieces
1328 * @return object
1329 */
1330 protected function resolveMiddleware($handler, $pieces)
1331 {
1332 if (is_object($handler)) {
1333 $handler = $this->wrapMiddleware($handler, $pieces);
1334 } elseif (is_string($handler)) {
1335 $handler = $handler . ':' . str_replace(' ', '', end($pieces));
1336 }
1337
1338 return $handler;
1339 }
1340
1341 /**
1342 * Create a class to wrap the middleware
1343 *
1344 * @param mixed $handler
1345 * @param array $pieces
1346 * @return object
1347 */
1348 protected function wrapMiddleware($handler, $pieces)
1349 {
1350 $params = str_replace(' ', '', end($pieces));
1351
1352 $params = explode(',', $params);
1353
1354 return new class ($handler, $params) {
1355 protected $handler, $params = null;
1356
1357 public function __construct($handler, $params) {
1358 $this->handler = $handler;
1359 $this->params = $params;
1360 }
1361
1362 public function handle($r, $next) {
1363 if (is_callable($this->handler)) {
1364 return ($this->handler)($r, $next, ...$this->params);
1365 } else {
1366 if (!method_exists($this->handler, 'handle')) {
1367 $class = get_class($this->handler);
1368 throw new InvalidArgumentException(
1369 "The {$class} must implement the handle method."
1370 );
1371 }
1372 return $this->handler->handle($r, $next, ...$this->params);
1373 }
1374 }
1375 };
1376 }
1377
1378 /**
1379 * Add the middleware in the stack
1380 *
1381 * @param array &$stack All callable middleware for the route
1382 * @param string $middleware
1383 * @return void
1384 */
1385 protected function addMiddlewareInTheStack(&$stack, $middleware)
1386 {
1387 if (!in_array($middleware, $stack)) {
1388 $stack[] = $middleware;
1389 }
1390 }
1391
1392 /**
1393 * Resolve the policy handler
1394 *
1395 * @param string $policyHandler
1396 * @return mixed
1397 */
1398 protected function getPolicyHandler($policyHandler)
1399 {
1400 if (!$policyHandler) {
1401 return [$this, 'defaultPolicyHandler'];
1402 }
1403
1404 if (is_callable($policyHandler)) {
1405 return $policyHandler;
1406 }
1407
1408 if (is_string($policyHandler)) {
1409
1410 if (function_exists($policyHandler)) {
1411 return $policyHandler;
1412 }
1413
1414 $policyHandlerFunction = substr(
1415 $policyHandler, strrpos($policyHandler, '\\') + 1
1416 );
1417
1418 if (function_exists($policyHandlerFunction)) {
1419 return $policyHandlerFunction;
1420 }
1421 }
1422
1423 if ($this->isPolicyHandlerParseable($policyHandler)) {
1424 return $policyHandler;
1425 }
1426
1427 if (is_string($policyHandler) && $this->handler instanceof Closure) {
1428
1429 if (class_exists($policyHandler)) {
1430
1431 $reflection = new ReflectionClass($policyHandler);
1432
1433 if ($reflection->hasMethod('verifyRequest')) {
1434 return $policyHandler . '@' . 'verifyRequest';
1435 }
1436 } elseif (function_exists($policyHandler)) {
1437 return $policyHandler;
1438 }
1439
1440 throw new InvalidArgumentException(
1441 'Explicit policy handler is required while using a closure as route callback.'
1442 );
1443 }
1444
1445 if ($policyHandler && !function_exists($policyHandler)) {
1446 [$_, $method] = is_array($this->handler)
1447 ? [$this->handler[0], $this->handler[1] ?? '__invoke']
1448 : Str::parseCallback($this->handler, '__invoke');
1449
1450 $policyHandler .= '@' . $method;
1451 }
1452
1453 return $policyHandler ?: [$this, 'defaultPolicyHandler'];
1454 }
1455
1456 /**
1457 * Check if the policy handler is parseable.
1458 *
1459 * @param string $policyHandler
1460 * @return boolean
1461 */
1462 protected function isPolicyHandlerParseable($policyHandler)
1463 {
1464 return (strpos($policyHandler, '@') === true
1465 || strpos($policyHandler, '::') === true);
1466 }
1467
1468 /**
1469 * Default/Fallback policy handler for the route
1470 *
1471 * @return bool
1472 */
1473 public function defaultPolicyHandler()
1474 {
1475 return true;
1476 }
1477
1478 /**
1479 * Parse the rest and permission/policy handlers
1480 *
1481 * @param \WP_REST_Request $request
1482 * @return null
1483 * @throws \BadMethodCallException
1484 */
1485 public function prepareCallbacks($request)
1486 {
1487 $handler = $this->app->parseRestHandler($this->handler, $this->namespace);
1488
1489 [$action, $controller] = $this->resolveHandlerDetails($handler);
1490
1491 $policyHandler = $this->resolvePolicyHandler();
1492
1493 $this->actionInfo = [
1494 'handler' => is_object($handler) ? $action : $handler,
1495 'controller' => $controller,
1496 'method' => $this->getMethodName($action, $handler),
1497 'path' => $this->uri,
1498 'http_method' => $request->get_method(),
1499 'full_uri' => $request->get_route(),
1500 'permission_callback' => $policyHandler,
1501 'compiled_url' => $this->compiled
1502 ];
1503
1504 $this->action = $handler;
1505
1506 if ($routeParameters = $this->getParameter()) {
1507 $this->substitutedParameters = $this->substituteParameters($routeParameters);
1508 }
1509
1510 return $this->action;
1511 }
1512
1513 /**
1514 * Get the method name to build action info.
1515 *
1516 * @param mixed $action
1517 * @param mixed $handler
1518 * @return string|null
1519 */
1520 protected function getMethodName($action, $handler)
1521 {
1522 $method = is_array($action) ? $action[1] ?? '__invoke' : null;
1523
1524 if (is_null($method) && is_object($handler)) {
1525 $method = '__invoke';
1526 }
1527
1528 return $method;
1529 }
1530
1531 /**
1532 * Resolve the handler details.
1533 *
1534 * @param mixed $handler
1535 * @return array
1536 */
1537 protected function resolveHandlerDetails($handler)
1538 {
1539 if ($handler instanceof Closure) {
1540 return ['Closure', null];
1541 }
1542
1543 if (is_object($handler)) {
1544 $class = get_class($handler);
1545 return [$class, $class];
1546 }
1547
1548 $handler = trim($handler, '\\');
1549 [$controller, $method] = Str::parseCallback($handler, '__invoke');
1550 $controllerName = $this->extractControllerName($controller);
1551
1552 return [[$controller, $method], $controllerName];
1553 }
1554
1555 /**
1556 * Extract the controller name from the FQCN.
1557 *
1558 * @param string $fqcn
1559 * @return string
1560 */
1561 protected function extractControllerName($fqcn)
1562 {
1563 $parts = explode('\\', $fqcn);
1564 return end($parts);
1565 }
1566
1567 /**
1568 * Parse and validate the policy handler.
1569 *
1570 * @return array
1571 */
1572 protected function resolvePolicyHandler()
1573 {
1574 try {
1575 $policyHandler = $this->app->parsePolicyHandler(
1576 $this->getPolicyHandler($this->policyHandler)
1577 );
1578
1579 if ($policyHandler) {
1580 $this->permissionHandler = $policyHandler;
1581
1582 // Adjust method if explicitly given in string policy handler
1583 if (is_string($this->policyHandler) && is_array($policyHandler) && isset($policyHandler[1])) {
1584 $pieces = explode('@', $this->policyHandler);
1585 if (isset($pieces[1])) {
1586 $this->permissionHandler[1] = $pieces[1];
1587 }
1588 }
1589
1590 if (!is_callable($this->permissionHandler)) {
1591 throw new Exception;
1592 }
1593 }
1594
1595 } catch (Exception $e) {
1596 throw $this->invalidPolicyHandlerException();
1597 }
1598
1599 // Convert object controller to class string for endpoint metadata
1600 if (is_array($policyHandler) && is_object($policyHandler[0])) {
1601 $policyHandler[0] = get_class($policyHandler[0]);
1602 }
1603
1604 return $policyHandler;
1605 }
1606
1607 /**
1608 * Build and throw an exception for invalid policy handlers.
1609 *
1610 * @throws \BadMethodCallException
1611 */
1612 protected function invalidPolicyHandlerException()
1613 {
1614 $pHandler = $this->policyHandler;
1615
1616 if (is_array($this->permissionHandler) && $this->permissionHandler) {
1617 $pHandler = is_object($this->permissionHandler[0])
1618 ? get_class($this->permissionHandler[0]) . ':' . $this->permissionHandler[1]
1619 : $this->permissionHandler[0] . ':' . $this->permissionHandler[1];
1620 }
1621
1622 return new BadMethodCallException(
1623 "The permission callback {$pHandler} is invalid or not callable."
1624 );
1625 }
1626
1627 /**
1628 * Get one or more route parameters
1629 * @param string $key
1630 *
1631 * @return mixed
1632 */
1633 public function getParameter($key = null)
1634 {
1635 if (is_null($this->parameters)) {
1636 $this->parameters = $this->app->request->get_url_params();
1637 }
1638
1639 return $key ? $this->parameters[$key] : $this->parameters;
1640 }
1641
1642 /**
1643 * Get the name of the route.
1644 *
1645 * @return string
1646 */
1647 public function getName()
1648 {
1649 return $this->name;
1650 }
1651
1652 /**
1653 * Get the url of the route.
1654 *
1655 * @return string
1656 */
1657 public function getUrl()
1658 {
1659 return $this->uri;
1660 }
1661
1662 /**
1663 * Get the url of the route.
1664 *
1665 * @return string
1666 */
1667 public function uri()
1668 {
1669 return $this->getUrl();
1670 }
1671
1672 /**
1673 * Dynamically access a route parameter.
1674 *
1675 * @param string $key
1676 * @return mixed
1677 */
1678 public function __get($key)
1679 {
1680 return $this->getParameter($key);
1681 }
1682 }
1683