PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.11.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.11.0
2.11.0 2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 All 78 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Http/Route.php +711 -124 1.0.922.11.0 View file →
@@ -3,24 +3,31 @@
3 3 namespace FluentCommunity\Framework\Http;
4 4
5 5 use Closure;
6 6 use Exception;
7 +use Throwable;
7 8 use WP_Error;
8 9 use WP_REST_Request;
9 10 use WP_REST_Response;
11 +use ReflectionClass;
10 12 use BadMethodCallException;
11 13 use InvalidArgumentException;
12 14 use FluentCommunity\Framework\Support\Arr;
15 +use FluentCommunity\Framework\Support\Str;
13 16 use FluentCommunity\Framework\Support\Pipeline;
14 17 use FluentCommunity\Framework\Http\Request\Request;
18 +use FluentCommunity\Framework\Http\Request\WPUserProxy;
19 +use FluentCommunity\Framework\Http\SubstituteParameters;
15 20 use FluentCommunity\Framework\Http\Middleware\RateLimiter;
16 21 use FluentCommunity\Framework\Validator\ValidationException;
17 22 use FluentCommunity\Framework\Database\Orm\ModelNotFoundException;
18 -use FluentCommunity\Framework\Response\Response as WPFluentResponse;
23 +use FluentCommunity\Framework\Foundation\Exceptions\HttpException;
24 +use FluentCommunity\Framework\Foundation\Exceptions\ExceptionHandler;
25 +use FluentCommunity\Framework\Http\Response\Response as WPFluentResponse;
19 26
20 27 class Route
21 28 {
22 - use SubstituteRouteParametersTrait;
29 + use SubstituteParameters;
23 30
24 31 /**
25 32 * Application Instance
26 33 * @var \FluentCommunity\Framework\Foundation\Application
@@ -27,8 +34,14 @@
27 34 */
28 35 protected $app = null;
29 36
30 37 /**
38 + * Route name
39 + * @var string
40 + */
41 + protected $name = null;
42 +
43 + /**
31 44 * Rest namespace from config
32 45 * @var string
33 46 */
34 47 protected $restNamespace = null;
@@ -33,8 +46,14 @@
33 46 */
34 47 protected $restNamespace = null;
35 48
36 49 /**
50 + * Whether this route should override existing routes at the same URI.
51 + * @var bool
52 + */
53 + protected $shouldOverride = false;
54 +
55 + /**
37 56 * Full URI
38 57 * @var string
39 58 */
40 59 protected $uri = null;
@@ -152,8 +171,22 @@
152 171 */
153 172 protected $signed = false;
154 173
155 174 /**
175 + * Route signature.
176 + *
177 + * @var array
178 + */
179 + protected $endpointSignature = [];
180 +
181 + /**
182 + * Response instance
183 + *
184 + * @var \WP_REST_Response
185 + */
186 + protected $response = null;
187 +
188 + /**
156 189 * Construct the route instance
157 190 *
158 191 * @param \FluentCommunity\Framework\Foundation\Application $app
159 192 * @param string $restNamespace
@@ -167,20 +200,19 @@
167 200 $this->restNamespace = $restNamespace;
168 201 $this->uri = $uri;
169 202 $this->handler = $handler;
170 203 $this->method = $method;
171 -
172 - $this->preparefrontendHandlers($handler);
173 204 }
174 205
175 206 /**
176 207 * Map the route to be used in front-end.
177 208 *
178 - * @param mixed $handler
179 - * @return null
209 + * @return self
180 210 */
181 - protected function preparefrontendHandlers($handler)
211 + public function preparefrontendHandlers()
182 212 {
213 + $handler = $this->handler;
214 +
183 215 $endpointsUrl = $this->app->config->get('app.slug') . '/__endpoints';
184 216
185 217 if (get_option('permalink_structure')) {
186 218 $url = $this->app->request->url();
@@ -187,37 +219,83 @@
187 219 } else {
188 220 $url = $this->app->request->query('rest_route');
189 221 }
190 222
191 - if (!str_contains($url ?? '', $endpointsUrl)) {
192 - return;
223 + if (
224 + !str_contains($url ?? '', $endpointsUrl)
225 + || $handler instanceof Closure
226 + ) {
227 + return $this;
193 228 }
194 229
195 - if ($handler instanceof Closure) {
196 - return;
197 - }
230 + [$controller, $cb] = Str::parseCallback($this->parseAction($handler));
198 231
199 - $action = trim($this->app->parseRestHandler($handler), '\\');
232 + $this->endpointSignature = [$controller, "_{$cb}"];
200 233
201 - [$controller, $cb] = explode('@', $action);
202 -
203 234 $controller = str_replace('\\', '.', $controller);
204 235
236 + // @phpstan-ignore-next-line
205 237 $endpoints = $this->app->endpoints;
206 238
207 239 $endpoints[$controller]["_{$cb}"] = [
208 240 'uri' => $this->uri,
209 - 'methods' => explode(',', $this->method)
241 + 'methods' => explode(',', $this->method),
242 + 'policy' => $this->getPolicyName()
210 243 ];
211 244
245 + // @phpstan-ignore-next-line
212 246 $this->app->endpoints = $endpoints;
247 +
248 + return $this;
213 249 }
214 250
215 251 /**
252 + * Get a display name for the route's policy handler.
253 + *
254 + * @return string|null
255 + */
256 + protected function getPolicyName()
257 + {
258 + if (!$this->policyHandler) {
259 + return null;
260 + }
261 +
262 + if ($this->policyHandler instanceof Closure) {
263 + return 'Closure';
264 + }
265 +
266 + $name = $this->policyHandler;
267 +
268 + if (is_string($name) && !$this->app->hasNamespace($name)) {
269 + $name = $this->app->__namespace__ . '\\App\\Http\\Policies\\' . $name;
270 + }
271 +
272 + return $name;
273 + }
274 +
275 + /**
276 + * Parse the action from the handler.
277 + *
278 + * @param mixed $handler
279 + * @return string
280 + */
281 + protected function parseAction($handler)
282 + {
283 + $action = $this->app->parseRestHandler($handler, $this->namespace);
284 + $action = trim($action, '\\');
285 +
286 + if (!str_contains($action, '@')) {
287 + $action .= '@__invoke';
288 + }
289 +
290 + return $action;
291 + }
292 +
293 + /**
216 294 * Alternative constructor
217 295 *
218 296 * @param \FluentCommunity\Framework\Foundation\Application $app
219 - * @param string $restNamespace
297 + * @param string $namespace
220 298 * @param string $uri
221 299 * @param string $handler
222 300 * @param string $method
223 301 * @return self
@@ -421,8 +499,21 @@
421 499 return $this;
422 500 }
423 501
424 502 /**
503 + * Set the default route policy.
504 + *
505 + * @return self
506 + */
507 + public function withDefaultPolicy()
508 + {
509 + return $this->withPolicy(
510 + // @phpstan-ignore-next-line
511 + $this->app->__namespace__.'\\App\\Http\\Policies\\Policy'
512 + );
513 + }
514 +
515 + /**
425 516 * Set the route policy
426 517 *
427 518 * @param mixed $handler
428 519 * @param string|null $method
@@ -441,15 +532,83 @@
441 532 debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4)
442 533 );
443 534 }
444 535
536 + return $this->addRouteInfo($handler);
537 + }
538 +
539 + /**
540 + * Check if the request is from CLI;
541 + *
542 + * @return bool
543 + */
544 + protected function fromCli()
545 + {
546 + $hash = $this->app->request->header('X-From-CLI');
547 +
548 + $slugHash = md5($this->app->config->get('app.slug'));
549 +
550 + return $hash === $slugHash;
551 + }
552 +
553 + /**
554 + * Add route information for CLI command.
555 + *
556 + * @param mixed $handler
557 + * @return self
558 + */
559 + protected function addRouteInfo($handler)
560 + {
561 + if (!$this->fromCli()) {
562 + return $this;
563 + }
564 +
565 + if ($handler instanceof Closure) {
566 + $policyHandler = 'Closure';
567 + } else {
568 + $policyHandler = $this->resolvePolicyHandler();
569 + if (is_array($policyHandler)) {
570 + $policyHandler = implode('@', $policyHandler);
571 + }
572 + }
573 +
574 + $this->injectProp('policy', $policyHandler);
575 +
445 576 return $this;
446 577 }
447 578
448 579 /**
580 + * Inject property into route infio.
581 + *
582 + * @param string $key
583 + * @param mixed $value
584 + * @return void
585 + */
586 + public function injectProp($key, $value)
587 + {
588 + if (!$this->endpointSignature) {
589 + return;
590 + }
591 +
592 + [$controller, $cbKey] = $this->endpointSignature;
593 +
594 + $controllerKey = str_replace('\\', '.', $controller);
595 +
596 + // @phpstan-ignore-next-line
597 + $endpoints = $this->app->endpoints;
598 +
599 + if (isset($endpoints[$controllerKey][$cbKey])) {
600 + $endpoints[$controllerKey][$cbKey][$key] = $value;
601 + // @phpstan-ignore-next-line
602 + $this->app->endpoints = $endpoints;
603 + }
604 + }
605 +
606 + /**
449 607 * Resolve and set policy with namespace for add-ons
450 608 *
451 - * @param null
609 + * @param array $backTrace
610 + * @return void
452 611 */
453 612 protected function setPolicyHandlerWithNamespace($backTrace)
454 613 {
455 614 $last = end($backTrace);
@@ -468,15 +627,49 @@
468 627 }
469 628 }
470 629
471 630 /**
472 - * Set the namespace for controller/action
631 + * Set the name for the route.
632 + *
633 + * @param string $name
634 + * @return self
635 + */
636 + public function name($name)
637 + {
638 + if (!$this->name) {
639 + $this->name = $name;
640 + } else {
641 + $this->name .= $name;
642 + }
643 +
644 + // @phpstan-ignore-next-line
645 + return $this->app->router->setNamedRoute($this->name, $this);
646 + }
647 +
648 + /**
649 + * Set the name for the route.
650 + *
651 + * @param string $name
652 + * @return null
653 + */
654 + public function withName($name)
655 + {
656 + $this->name = implode('', $name);
657 + }
658 +
659 + /**
660 + * Set the namespace for controller/action.
661 + *
473 662 * @param string $ns
474 663 * @return null
475 664 */
476 665 public function withNamespace($ns)
477 666 {
478 - $this->namespace = implode('\\', $ns);
667 + if (is_array($ns)) {
668 + $this->namespace = implode('\\', $ns);
669 + } else {
670 + $this->namespace = trim($ns, '\\');
671 + }
479 672 }
480 673
481 674 /**
482 675 * Sign the route.
@@ -499,8 +692,12 @@
499 692 * @return $this
500 693 */
501 694 public function rateLimit($limit, $interval)
502 695 {
696 + // Since the rate limiter is applied twice because
697 + // WordPress sends an extra request for every
698 + // request, so we need to double the limit.
699 +
503 700 $rateLimiter = new RateLimiter($limit * 2, $interval);
504 701
505 702 $this->middleware('before', $rateLimiter);
506 703
@@ -546,15 +743,48 @@
546 743 * @return null
547 744 */
548 745 public function register()
549 746 {
747 + $this->updateRouteOptions();
748 +
749 + return register_rest_route(
750 + $this->restNamespace,
751 + $this->getRouteUri(),
752 + $this->getOptions(),
753 + $this->shouldOverride
754 + );
755 + }
756 +
757 + /**
758 + * Update route options before registering.
759 + *
760 + * @return void
761 + */
762 + protected function updateRouteOptions()
763 + {
550 764 $this->setOptions();
765 + }
551 766
552 - $uri = '/' . trim($this->compileRoute($this->uri), '/');
767 + /**
768 + * Get normalized uri for the current route.
769 + *
770 + * @return string
771 + */
772 + protected function getRouteUri()
773 + {
774 + return '/' . trim($this->compileRoute($this->uri), '/');
775 + }
553 776
554 - return register_rest_route(
555 - $this->restNamespace, $uri, $this->getOptions()
556 - );
777 + /**
778 + * Mark this route to override any existing route at the same URI.
779 + *
780 + * @return $this
781 + */
782 + public function override()
783 + {
784 + $this->shouldOverride = true;
785 +
786 + return $this;
557 787 }
558 788
559 789 /**
560 790 * Set route options
@@ -562,9 +792,21 @@
562 792 * @return null
563 793 */
564 794 protected function setOptions()
565 795 {
566 - $this->options = [
796 + $this->options = array_merge(
797 + $this->options, $this->getDefaultOptions()
798 + );
799 + }
800 +
801 + /**
802 + * Get default options.
803 + *
804 + * @return array
805 + */
806 + protected function getDefaultOptions()
807 + {
808 + return [
567 809 [
568 810 'methods' => $this->method,
569 811 'callback' => [$this, 'callback'],
570 812 'permission_callback' => [$this, 'permissionCallback'],
@@ -569,9 +811,8 @@
569 811 'callback' => [$this, 'callback'],
570 812 'permission_callback' => [$this, 'permissionCallback'],
571 813 'args' => [],
572 814 ],
573 - 'schema' => [$this, 'getSchema'],
574 815 ];
575 816 }
576 817
577 818 /**
@@ -576,16 +817,16 @@
576 817
577 818 /**
578 819 * Generate and return the schema for the route.
579 820 *
580 - * @return \Closure
821 + * @return self
581 822 * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/schema
582 823 */
583 - public function getSchema()
824 + public function schema($schema)
584 825 {
585 - return function () {
586 - return [];
587 - };
826 + $this->options['schema'] = fn() => $schema;
827 +
828 + return $this;
588 829 }
589 830
590 831 /**
591 832 * Get item from predefined regex
@@ -650,17 +891,19 @@
650 891
651 892 /**
652 893 * Route handler
653 894 *
654 - * @return mixed
895 + * @return \WP_REST_Response
655 896 */
656 897 public function callback()
657 898 {
658 899 try {
659 - return $this->handleAfterMiddleware(
660 - $response = $this->dispatchRouteAction()
900 + $this->response = $this->handleAfterMiddleware(
901 + $this->dispatchRouteAction()
661 902 );
662 903
904 + return $this->handleResponse($this->response);
905 +
663 906 } catch (ValidationException $e) {
664 907 return $this->app->response->sendError(
665 908 $e->errors(), $e->getCode()
666 909 );
@@ -667,19 +910,163 @@
667 910 } catch (ModelNotFoundException $e) {
668 911 return $this->app->response->sendError([
669 912 'message' => $e->getMessage()
670 913 ], 404);
671 - } catch (Exception $e) {
672 - return $this->app->response->sendError([
673 - 'message' => $e->getMessage()
674 - ], $e->getCode() ?: 500);
914 + } catch (HttpException $e) {
915 + return $this->renderHttpException($e);
916 + } catch (Throwable $e) {
917 + $headers = $this->response ? $this->response->get_headers() : [];
918 +
919 + // Consult the plugin's ExceptionHandler registry BEFORE the
920 + // production sanitizer. A registered renderable may return
921 + // either an HttpException (rendered with full status + safe
922 + // message) or a WP_REST_Response (returned verbatim). Null /
923 + // no-match falls through to handleUnknownException — the
924 + // sanitization default is preserved for any exception not
925 + // explicitly opted in.
926 + if ($mapped = $this->mapToHandlerResponse($e)) {
927 + return $mapped;
928 + }
929 +
930 + return $this->handleUnknownException($e, $headers);
675 931 }
676 932 }
677 933
678 934 /**
935 + * Run the bound `ExceptionHandler` over `$e` and convert its result
936 + * to a `WP_REST_Response`, or `null` if the handler has nothing for
937 + * this exception (in which case the caller falls through to the
938 + * sanitizer).
939 + *
940 + * Returns an `HttpException` result through `renderHttpException()`
941 + * so observability + headers + the `{code, message, data}` shape
942 + * stay consistent with the dedicated `HttpException` catch arm.
943 + * A `WP_REST_Response` is returned verbatim — the renderer claimed
944 + * full control over the response shape; we still fire
945 + * `fluent_exception` so observability listeners see the original
946 + * exception.
947 + *
948 + * @param \Throwable $e
949 + * @return \WP_REST_Response|null
950 + */
951 + protected function mapToHandlerResponse(Throwable $e)
952 + {
953 + if (!$this->app->bound(ExceptionHandler::class)) {
954 + return null;
955 + }
956 +
957 + $handler = $this->app->make(ExceptionHandler::class);
958 +
959 + if (!$handler instanceof ExceptionHandler) {
960 + return null;
961 + }
962 +
963 + $result = $handler->render($e, $this->app);
964 +
965 + if ($result instanceof HttpException) {
966 + return $this->renderHttpException($result);
967 + }
968 +
969 + if ($result instanceof WP_REST_Response) {
970 + $this->fireExceptionEvent($e);
971 + return $result;
972 + }
973 +
974 + return null;
975 + }
976 +
977 + /**
978 + * Handle response from route.
979 + *
980 + * @param \WP_REST_Response $response
981 + * @return \WP_REST_Response
982 + */
983 + protected function handleResponse($response)
984 + {
985 + return $response;
986 + }
987 +
988 + /**
989 + * Throw an exception based on the status code.
990 + *
991 + * @param string $message
992 + * @param int $status
993 + * @return null
994 + * @throws \Exception
995 + */
996 + protected function throwException($message, $status)
997 + {
998 + $class = sprintf(
999 + 'WpOrg\Requests\Exception\Http\Status%d', $status
1000 + );
1001 +
1002 + if (!class_exists($class)) {
1003 + $class = 'WpOrg\Requests\Exception\Http';
1004 + }
1005 +
1006 + throw new $class($message, $status);
1007 + }
1008 +
1009 + /**
1010 + * Handle exception and send error response.
1011 + *
1012 + * @param Throwable $e
1013 + * @return \WP_REST_Response
1014 + */
1015 + protected function handleUnknownException(Throwable $e, $headers = [])
1016 + {
1017 + $data = [];
1018 +
1019 + $this->fireExceptionEvent($e);
1020 +
1021 + // Production sanitization: client-facing message must not leak
1022 + // PDO / HTTP-client / file-system internals. The real message
1023 + // ships to fluent_exception listeners (Night Watcher / bridge)
1024 + // via fireExceptionEvent above, so observability is preserved.
1025 + if ($this->app->isDebugOn()) {
1026 + $data = [
1027 + 'file' => $e->getFile(),
1028 + 'line' => $e->getLine(),
1029 + ];
1030 +
1031 + $message = $e->getMessage();
1032 + } else {
1033 + $message = 'An internal error occurred.';
1034 + }
1035 +
1036 + return $this->app->response->sendError([
1037 + 'code' => 'plugin_exception',
1038 + 'data' => $data,
1039 + 'message' => $message,
1040 + ], $e->getCode() ?: 500, $headers);
1041 + }
1042 +
1043 + /**
1044 + * Render an HttpException to a sanitization-free response.
1045 + *
1046 + * HttpException is the opt-in contract for "I authored this message,
1047 + * it is safe to ship to the client". Bypasses handleUnknownException's
1048 + * production sanitization but still fires fluent_exception for
1049 + * observability so listeners see every thrown HttpException.
1050 + *
1051 + * @param HttpException $e
1052 + * @return \WP_REST_Response
1053 + */
1054 + protected function renderHttpException(HttpException $e)
1055 + {
1056 + $this->fireExceptionEvent($e);
1057 +
1058 + return $this->app->response->sendError([
1059 + 'code' => $e->getErrorCode(),
1060 + 'message' => $e->getMessage(),
1061 + 'data' => $e->getData(),
1062 + ], $e->getStatusCode(), $e->getHeaders());
1063 + }
1064 +
1065 + /**
679 1066 * Dispatch the route action.
680 1067 *
681 - * @return mixed
1068 + * @return \WP_REST_Response
682 1069 */
683 1070 protected function dispatchRouteAction()
684 1071 {
685 1072 $response = $this->app->call(
@@ -706,15 +1093,12 @@
706 1093 protected function handleAfterMiddleware($response)
707 1094 {
708 1095 if (!$this->skipMiddleware) {
709 1096 $response = $this->app->make(Pipeline::class)
710 - ->send($response)
1097 + ->send(new WPFluentResponse($response))
711 1098 ->through($this->collectMiddleWare('after'))
712 - ->then(function ($response) {
713 - if (!$response instanceof WP_REST_Response) {
714 - $response = new WP_REST_Response($response);
715 - }
716 - return $response;
1099 + ->then(function($response) {
1100 + return $this->normalize($response);
717 1101 });
718 1102
719 1103 if (!$response) {
720 1104 $response = $this->app->request->abort();
@@ -724,8 +1108,75 @@
724 1108 return $response;
725 1109 }
726 1110
727 1111 /**
1112 + * Normalize the response.
1113 + *
1114 + * @param mixed $response
1115 + * @return mixed
1116 + */
1117 + protected function normalize($response)
1118 + {
1119 + if ($response instanceof WPFluentResponse) {
1120 + $response = $response->toArray();
1121 + }
1122 +
1123 + if (!$response instanceof WP_REST_Response) {
1124 + return new WP_REST_Response($response);
1125 + }
1126 +
1127 + return $response;
1128 + }
1129 +
1130 + /**
1131 + * Fire exception action hook.
1132 + *
1133 + * @param Exception $exception
1134 + * @return void
1135 + */
1136 + protected function fireExceptionEvent($exception)
1137 + {
1138 + // Reentrancy guard: a fluent_exception listener that itself triggers
1139 + // an exception path must not re-enter this method and recurse. Reset
1140 + // in finally so subsequent (sequential) calls proceed normally.
1141 + static $firing = false;
1142 +
1143 + if ($firing) {
1144 + return;
1145 + }
1146 +
1147 + if ($this->app->isDebugOn() || defined('FLUENT_BRIDGE_SECRET')) {
1148 + $message = sprintf(
1149 + "%s in %s:%d\nStack trace:\n%s\n",
1150 + $exception->getMessage(),
1151 + $exception->getFile(),
1152 + $exception->getLine(),
1153 + $exception->getTraceAsString()
1154 + );
1155 +
1156 + error_log($message);
1157 + }
1158 +
1159 + $firing = true;
1160 +
1161 + try {
1162 + $this->app->doAction('fluent_exception', $exception);
1163 + } catch (Throwable $listenerError) {
1164 + // Listener-throw isolation: a buggy fluent_exception listener
1165 + // (DB down, disk full) must not escape and crash the response.
1166 + // Log under the same gate; never re-fire fluent_exception here
1167 + // — that would be the cascade we are protecting against.
1168 + if ($this->app->isDebugOn() || defined('FLUENT_BRIDGE_SECRET')) {
1169 + error_log(
1170 + 'fluent_exception listener failed: ' . $listenerError->getMessage()
1171 + );
1172 + }
1173 + } finally {
1174 + $firing = false;
1175 + }
1176 + }
1177 +
1178 + /**
728 1179 * Permission callback for route
729 1180 * @param \WP_REST_Request $wpRestRequest
730 1181 * @return mixed
731 1182 */
@@ -731,8 +1182,10 @@
731 1182 */
732 1183 public function permissionCallback($wpRestRequest)
733 1184 {
734 1185 try {
1186 + $this->parameters = null;
1187 + $this->substitutedParameters = null;
735 1188 $this->app->instance('route', $this);
736 1189 $this->app->instance('wprestrequest', $wpRestRequest);
737 1190 $this->app->request->mergeInputsFromRestRequest($wpRestRequest);
738 1191 $this->prepareCallbacks($this->app->request);
@@ -767,9 +1220,9 @@
767 1220 }
768 1221
769 1222 return $response;
770 1223
771 - } catch (Exception $e) {
1224 + } catch (Throwable $e) {
772 1225 return new WP_Error(
773 1226 'Permission Callback Error',
774 1227 $e->getMessage(), [
775 1228 'status' => $e->getCode() ?: 403
@@ -804,23 +1257,62 @@
804 1257 }
805 1258 }
806 1259
807 1260 /**
808 - * Dispatches the permission handler
1261 + * Dispatches the permission handler.
809 1262 *
810 1263 * @return bool|null
811 1264 */
812 1265 protected function dispatchPermissionHandler()
813 1266 {
814 - if ($this->permissionHandler) {
815 - return $this->app->call(
816 - $this->permissionHandler,
817 - $this->getControllerParameters()
818 - );
1267 + if (!$this->permissionHandler) {
1268 + return true;
819 1269 }
1270 +
1271 + $isValid = $this->app->call(
1272 + $this->permissionHandler,
1273 + $this->getControllerParameters()
1274 + );
1275 +
1276 + if (is_object($isValid)) {
1277 + if ($this->isUser($isValid)) {
1278 + $isValid = $isValid->id();
1279 + } else {
1280 + $this->throwInvalidPolicy();
1281 + }
1282 + }
1283 +
1284 + if (!is_bool($isValid) && !is_int($isValid) && !is_null($isValid)) {
1285 + $this->throwInvalidPolicy();
1286 + }
1287 +
1288 + return (bool) $isValid;
820 1289 }
821 1290
822 1291 /**
1292 + * Checks if the user is an instance of WPUserProxy.
1293 + *
1294 + * @param WPUserProxy $user
1295 + * @return bool
1296 + */
1297 + protected function isUser($user)
1298 + {
1299 + return $user instanceof WPUserProxy;
1300 + }
1301 +
1302 + /**
1303 + * Throw invalid policy handling exception.
1304 + *
1305 + * @return InvalidArgumentException
1306 + */
1307 + protected function throwInvalidPolicy()
1308 + {
1309 + throw new InvalidArgumentException(
1310 + 'The policy must return a boolean, integer, null, or a WPUserProxy instance.', 500
1311 + );
1312 + }
1313 +
1314 + /**
823 1315 * Gether route params after substituted the params
824 1316 *
825 1317 * @return array
826 1318 */
@@ -829,9 +1321,9 @@
829 1321 $routeParameters = [];
830 1322
831 1323 if (!$this->substitutedParameters) {
832 1324 if ($routeParameters = $this->getParameter()) {
833 - $routeParameters = $this->SubstituteParameters($routeParameters);
1325 + $routeParameters = $this->substituteParameters($routeParameters);
834 1326 }
835 1327 } else {
836 1328 $routeParameters = $this->substitutedParameters;
837 1329 }
@@ -856,9 +1348,11 @@
856 1348 * @return array
857 1349 */
858 1350 protected function collectMiddleWare($type = 'before')
859 1351 {
860 - $middleware = $this->app['config']->get('middleware', []);
1352 + $middleware = $this->app->bound('http.middleware')
1353 + ? $this->app['http.middleware']
1354 + : [];
861 1355
862 1356 $callableMiddleware = Arr::get($middleware, "global.{$type}", []);
863 1357
864 1358 $routeArray = [];
@@ -887,9 +1381,9 @@
887 1381 if (isset($handler)) {
888 1382 $this->addMiddlewareInTheStack($callableMiddleware, $handler);
889 1383 } else {
890 1384 if (isset($key)) {
891 - $mpath = 'config.middleware.route.' . $type;
1385 + $mpath = 'app/Http/middleware.php route.' . $type;
892 1386 $msg = "No middleware is assigned for the key: {$key} in {$mpath} array.";
893 1387 } else {
894 1388 $msg = "Could't resolve middleware.";
895 1389 }
@@ -908,9 +1402,8 @@
908 1402 * @return \Closure
909 1403 */
910 1404 protected function resolveMiddlewareFrom($class)
911 1405 {
912 - return (new $class);
913 1406 return static function ($r, $next, ...$params) use ($class) {
914 1407 return (new $class)->handle($r, $next, ...$params);
915 1408 };
916 1409 }
@@ -918,9 +1411,9 @@
918 1411 /**
919 1412 * Resolve the middleware
920 1413 *
921 1414 * @param mixed $handler
922 - * @param aray $pieces
1415 + * @param array $pieces
923 1416 * @return object
924 1417 */
925 1418 protected function resolveMiddleware($handler, $pieces)
926 1419 {
@@ -936,9 +1429,9 @@
936 1429 /**
937 1430 * Create a class to wrap the middleware
938 1431 *
939 1432 * @param mixed $handler
940 - * @param aray $pieces
1433 + * @param array $pieces
941 1434 * @return object
942 1435 */
943 1436 protected function wrapMiddleware($handler, $pieces)
944 1437 {
@@ -948,16 +1441,14 @@
948 1441
949 1442 return new class ($handler, $params) {
950 1443 protected $handler, $params = null;
951 1444
952 - public function __construct($handler, $params)
953 - {
1445 + public function __construct($handler, $params) {
954 1446 $this->handler = $handler;
955 1447 $this->params = $params;
956 1448 }
957 1449
958 - public function handle($r, $next)
959 - {
1450 + public function handle($r, $next) {
960 1451 if (is_callable($this->handler)) {
961 1452 return ($this->handler)($r, $next, ...$this->params);
962 1453 } else {
963 1454 if (!method_exists($this->handler, 'handle')) {
@@ -975,9 +1466,10 @@
975 1466 /**
976 1467 * Add the middleware in the stack
977 1468 *
978 1469 * @param array &$stack All callable middleware for the route
979 - * @param null
1470 + * @param string $middleware
1471 + * @return void
980 1472 */
981 1473 protected function addMiddlewareInTheStack(&$stack, $middleware)
982 1474 {
983 1475 if (!in_array($middleware, $stack)) {
@@ -1006,9 +1498,11 @@
1006 1498 if (function_exists($policyHandler)) {
1007 1499 return $policyHandler;
1008 1500 }
1009 1501
1010 - $policyHandlerFunction = substr($policyHandler, strrpos($policyHandler, '\\') + 1);
1502 + $policyHandlerFunction = substr(
1503 + $policyHandler, strrpos($policyHandler, '\\') + 1
1504 + );
1011 1505
1012 1506 if (function_exists($policyHandlerFunction)) {
1013 1507 return $policyHandlerFunction;
1014 1508 }
@@ -1021,15 +1515,12 @@
1021 1515 if (is_string($policyHandler) && $this->handler instanceof Closure) {
1022 1516
1023 1517 if (class_exists($policyHandler)) {
1024 1518
1025 - $reflection = new \ReflectionClass($policyHandler);
1519 + $reflection = new ReflectionClass($policyHandler);
1026 1520
1027 1521 if ($reflection->hasMethod('verifyRequest')) {
1028 -
1029 - $policyHandler = $policyHandler . '@' . 'verifyRequest';
1030 -
1031 - return $policyHandler;
1522 + return $policyHandler . '@' . 'verifyRequest';
1032 1523 }
1033 1524 } elseif (function_exists($policyHandler)) {
1034 1525 return $policyHandler;
1035 1526 }
@@ -1039,23 +1530,28 @@
1039 1530 );
1040 1531 }
1041 1532
1042 1533 if ($policyHandler && !function_exists($policyHandler)) {
1043 - if (is_string($this->handler) && strpos($this->handler, '@') !== false) {
1044 - list($_, $method) = explode('@', $this->handler);
1045 - $policyHandler = $policyHandler . '@' . $method;
1046 - } else if (is_array($this->handler)) {
1047 - $policyHandler = $policyHandler . '@' . $this->handler[1];
1048 - }
1534 + [$_, $method] = is_array($this->handler)
1535 + ? [$this->handler[0], $this->handler[1] ?? '__invoke']
1536 + : Str::parseCallback($this->handler, '__invoke');
1537 +
1538 + $policyHandler .= '@' . $method;
1049 1539 }
1050 1540
1051 1541 return $policyHandler ?: [$this, 'defaultPolicyHandler'];
1052 1542 }
1053 1543
1544 + /**
1545 + * Check if the policy handler is parseable.
1546 + *
1547 + * @param string $policyHandler
1548 + * @return boolean
1549 + */
1054 1550 protected function isPolicyHandlerParseable($policyHandler)
1055 1551 {
1056 - return (strpos($policyHandler, '@') === true
1057 - || strpos($policyHandler, '::') === true);
1552 + return (strpos($policyHandler, '@') !== false
1553 + || strpos($policyHandler, '::') !== false);
1058 1554 }
1059 1555
1060 1556 /**
1061 1557 * Default/Fallback policy handler for the route
@@ -1075,22 +1571,95 @@
1075 1571 * @throws \BadMethodCallException
1076 1572 */
1077 1573 public function prepareCallbacks($request)
1078 1574 {
1079 - $handler = $this->app->parseRestHandler(
1080 - $this->handler, $this->namespace
1081 - );
1575 + $handler = $this->app->parseRestHandler($this->handler, $this->namespace);
1082 1576
1577 + [$action, $controller] = $this->resolveHandlerDetails($handler);
1578 +
1579 + $policyHandler = $this->resolvePolicyHandler();
1580 +
1581 + $this->actionInfo = [
1582 + 'handler' => is_object($handler) ? $action : $handler,
1583 + 'controller' => $controller,
1584 + 'method' => $this->getMethodName($action, $handler),
1585 + 'path' => $this->uri,
1586 + 'http_method' => $request->get_method(),
1587 + 'full_uri' => $request->get_route(),
1588 + 'permission_callback' => $policyHandler,
1589 + 'compiled_url' => $this->compiled
1590 + ];
1591 +
1592 + $this->action = $handler;
1593 +
1594 + if ($routeParameters = $this->getParameter()) {
1595 + $this->substitutedParameters = $this->substituteParameters($routeParameters);
1596 + }
1597 +
1598 + return $this->action;
1599 + }
1600 +
1601 + /**
1602 + * Get the method name to build action info.
1603 + *
1604 + * @param mixed $action
1605 + * @param mixed $handler
1606 + * @return string|null
1607 + */
1608 + protected function getMethodName($action, $handler)
1609 + {
1610 + $method = is_array($action) ? $action[1] ?? '__invoke' : null;
1611 +
1612 + if (is_null($method) && is_object($handler)) {
1613 + $method = '__invoke';
1614 + }
1615 +
1616 + return $method;
1617 + }
1618 +
1619 + /**
1620 + * Resolve the handler details.
1621 + *
1622 + * @param mixed $handler
1623 + * @return array
1624 + */
1625 + protected function resolveHandlerDetails($handler)
1626 + {
1083 1627 if ($handler instanceof Closure) {
1084 - $action = 'Closure';
1085 - $controller = null;
1086 - } else {
1087 - $handler = trim($handler, '\\');
1088 - $action = explode('@', $handler);
1089 - $pieces = explode('\\', $action[0]);
1090 - $controller = end($pieces);
1628 + return ['Closure', null];
1091 1629 }
1092 1630
1631 + if (is_object($handler)) {
1632 + $class = get_class($handler);
1633 + return [$class, $class];
1634 + }
1635 +
1636 + $handler = trim($handler, '\\');
1637 + [$controller, $method] = Str::parseCallback($handler, '__invoke');
1638 + $controllerName = $this->extractControllerName($controller);
1639 +
1640 + return [[$controller, $method], $controllerName];
1641 + }
1642 +
1643 + /**
1644 + * Extract the controller name from the FQCN.
1645 + *
1646 + * @param string $fqcn
1647 + * @return string
1648 + */
1649 + protected function extractControllerName($fqcn)
1650 + {
1651 + $parts = explode('\\', $fqcn);
1652 + return end($parts);
1653 + }
1654 +
1655 + /**
1656 + * Parse and validate the policy handler.
1657 + *
1658 + * @return array
1659 + */
1660 + protected function resolvePolicyHandler()
1661 + {
1093 1662 try {
1094 1663 $policyHandler = $this->app->parsePolicyHandler(
1095 1664 $this->getPolicyHandler($this->policyHandler)
1096 1665 );
@@ -1097,16 +1666,13 @@
1097 1666
1098 1667 if ($policyHandler) {
1099 1668 $this->permissionHandler = $policyHandler;
1100 1669
1101 - // Adjust policy handler if the method was explicitly given
1102 - if (is_string($this->policyHandler)) {
1103 - if (is_array($policyHandler) && isset($policyHandler[1])) {
1104 - if ($pieces = explode('@', $this->policyHandler)) {
1105 - if (isset($pieces[1])) {
1106 - $this->permissionHandler[1] = $pieces[1];
1107 - }
1108 - }
1670 + // Adjust method if explicitly given in string policy handler
1671 + if (is_string($this->policyHandler) && is_array($policyHandler) && isset($policyHandler[1])) {
1672 + $pieces = explode('@', $this->policyHandler);
1673 + if (isset($pieces[1])) {
1674 + $this->permissionHandler[1] = $pieces[1];
1109 1675 }
1110 1676 }
1111 1677
1112 1678 if (!is_callable($this->permissionHandler)) {
@@ -1114,46 +1680,37 @@
1114 1680 }
1115 1681 }
1116 1682
1117 1683 } catch (Exception $e) {
1118 - $pHandler = $this->policyHandler;
1119 - if (is_array($this->permissionHandler) && $this->permissionHandler) {
1120 - $pHandler = is_object($this->permissionHandler[0]) ?
1121 - get_class($this->permissionHandler[0]) . ':' . $this->permissionHandler[1] :
1122 - $this->permissionHandler[0] . ':' . $this->permissionHandler[1];
1123 - }
1124 -
1125 - throw new BadMethodCallException(
1126 - "The permission callback {$pHandler} is invalid or not callable."
1127 - );
1684 + throw $this->invalidPolicyHandlerException();
1128 1685 }
1129 1686
1130 - if (is_array($policyHandler)) {
1687 + // Convert object controller to class string for endpoint metadata
1688 + if (is_array($policyHandler) && is_object($policyHandler[0])) {
1131 1689 $policyHandler[0] = get_class($policyHandler[0]);
1132 1690 }
1133 1691
1134 - $this->actionInfo = [
1135 - 'handler' => is_object($handler) ? $action : $handler,
1136 - 'controller' => $controller,
1137 - 'method' => is_array($action) ? $action[1] : null,
1138 - 'path' => $this->uri,
1139 - 'http_method' => $request->get_method(),
1140 - 'full_uri' => $request->get_route(),
1141 - 'permission_callback' => $policyHandler,
1142 - 'compiled_url' => $this->compiled
1143 - ];
1692 + return $policyHandler;
1693 + }
1144 1694
1695 + /**
1696 + * Build and throw an exception for invalid policy handlers.
1697 + *
1698 + * @throws \BadMethodCallException
1699 + */
1700 + protected function invalidPolicyHandlerException()
1701 + {
1702 + $pHandler = $this->policyHandler;
1145 1703
1146 - $this->action = $handler;
1147 -
1148 - if ($routeParameters = $this->getParameter()) {
1149 - $this->substitutedParameters = $this->SubstituteParameters(
1150 - $routeParameters
1151 - );
1704 + if (is_array($this->permissionHandler) && $this->permissionHandler) {
1705 + $pHandler = is_object($this->permissionHandler[0])
1706 + ? get_class($this->permissionHandler[0]) . ':' . $this->permissionHandler[1]
1707 + : $this->permissionHandler[0] . ':' . $this->permissionHandler[1];
1152 1708 }
1153 1709
1154 -
1155 - return $this->action;
1710 + return new BadMethodCallException(
1711 + "The permission callback {$pHandler} is invalid or not callable."
1712 + );
1156 1713 }
1157 1714
1158 1715 /**
1159 1716 * Get one or more route parameters
@@ -1167,8 +1724,38 @@
1167 1724 $this->parameters = $this->app->request->get_url_params();
1168 1725 }
1169 1726
1170 1727 return $key ? $this->parameters[$key] : $this->parameters;
1728 + }
1729 +
1730 + /**
1731 + * Get the name of the route.
1732 + *
1733 + * @return string
1734 + */
1735 + public function getName()
1736 + {
1737 + return $this->name;
1738 + }
1739 +
1740 + /**
1741 + * Get the url of the route.
1742 + *
1743 + * @return string
1744 + */
1745 + public function getUrl()
1746 + {
1747 + return $this->uri;
1748 + }
1749 +
1750 + /**
1751 + * Get the url of the route.
1752 + *
1753 + * @return string
1754 + */
1755 + public function uri()
1756 + {
1757 + return $this->getUrl();
1171 1758 }
1172 1759
1173 1760 /**
1174 1761 * Dynamically access a route parameter.