PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.21
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.21
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / vendor / wpfluent / framework / src / WPFluent / Http / Route.php

Route.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.21, at vendor/wpfluent/framework/src/WPFluent/Http/Route.php

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