PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Http/Route.php +707 -124 1.10.02 → 2.5.0 View file →
@@ -3,24 +3,31 @@
3 3 namespace FluentBooking\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 FluentBooking\Framework\Support\Arr;
15 +use FluentBooking\Framework\Support\Str;
13 16 use FluentBooking\Framework\Support\Pipeline;
14 17 use FluentBooking\Framework\Http\Request\Request;
18 +use FluentBooking\Framework\Http\Request\WPUserProxy;
19 +use FluentBooking\Framework\Http\SubstituteParameters;
15 20 use FluentBooking\Framework\Http\Middleware\RateLimiter;
16 21 use FluentBooking\Framework\Validator\ValidationException;
17 22 use FluentBooking\Framework\Database\Orm\ModelNotFoundException;
18 -use FluentBooking\Framework\Response\Response as WPFluentResponse;
23 +use FluentBooking\Framework\Foundation\Exceptions\HttpException;
24 +use FluentBooking\Framework\Foundation\Exceptions\ExceptionHandler;
25 +use FluentBooking\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 \FluentBooking\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 \FluentBooking\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 \FluentBooking\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.
@@ -550,15 +743,48 @@
550 743 * @return null
551 744 */
552 745 public function register()
553 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 + {
554 764 $this->setOptions();
765 + }
555 766
556 - $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 + }
557 776
558 - return register_rest_route(
559 - $this->restNamespace, $uri, $this->getOptions()
560 - );
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;
561 787 }
562 788
563 789 /**
564 790 * Set route options
@@ -566,9 +792,21 @@
566 792 * @return null
567 793 */
568 794 protected function setOptions()
569 795 {
570 - $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 [
571 809 [
572 810 'methods' => $this->method,
573 811 'callback' => [$this, 'callback'],
574 812 'permission_callback' => [$this, 'permissionCallback'],
@@ -573,9 +811,8 @@
573 811 'callback' => [$this, 'callback'],
574 812 'permission_callback' => [$this, 'permissionCallback'],
575 813 'args' => [],
576 814 ],
577 - 'schema' => [$this, 'getSchema'],
578 815 ];
579 816 }
580 817
581 818 /**
@@ -580,16 +817,16 @@
580 817
581 818 /**
582 819 * Generate and return the schema for the route.
583 820 *
584 - * @return \Closure
821 + * @return self
585 822 * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/schema
586 823 */
587 - public function getSchema()
824 + public function schema($schema)
588 825 {
589 - return function () {
590 - return [];
591 - };
826 + $this->options['schema'] = fn() => $schema;
827 +
828 + return $this;
592 829 }
593 830
594 831 /**
595 832 * Get item from predefined regex
@@ -654,17 +891,19 @@
654 891
655 892 /**
656 893 * Route handler
657 894 *
658 - * @return mixed
895 + * @return \WP_REST_Response
659 896 */
660 897 public function callback()
661 898 {
662 899 try {
663 - return $this->handleAfterMiddleware(
664 - $response = $this->dispatchRouteAction()
900 + $this->response = $this->handleAfterMiddleware(
901 + $this->dispatchRouteAction()
665 902 );
666 903
904 + return $this->handleResponse($this->response);
905 +
667 906 } catch (ValidationException $e) {
668 907 return $this->app->response->sendError(
669 908 $e->errors(), $e->getCode()
670 909 );
@@ -671,19 +910,163 @@
671 910 } catch (ModelNotFoundException $e) {
672 911 return $this->app->response->sendError([
673 912 'message' => $e->getMessage()
674 913 ], 404);
675 - } catch (Exception $e) {
676 - return $this->app->response->sendError([
677 - 'message' => $e->getMessage()
678 - ], $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);
679 931 }
680 932 }
681 933
682 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 + /**
683 1066 * Dispatch the route action.
684 1067 *
685 - * @return mixed
1068 + * @return \WP_REST_Response
686 1069 */
687 1070 protected function dispatchRouteAction()
688 1071 {
689 1072 $response = $this->app->call(
@@ -710,15 +1093,12 @@
710 1093 protected function handleAfterMiddleware($response)
711 1094 {
712 1095 if (!$this->skipMiddleware) {
713 1096 $response = $this->app->make(Pipeline::class)
714 - ->send($response)
1097 + ->send(new WPFluentResponse($response))
715 1098 ->through($this->collectMiddleWare('after'))
716 - ->then(function ($response) {
717 - if (!$response instanceof WP_REST_Response) {
718 - $response = new WP_REST_Response($response);
719 - }
720 - return $response;
1099 + ->then(function($response) {
1100 + return $this->normalize($response);
721 1101 });
722 1102
723 1103 if (!$response) {
724 1104 $response = $this->app->request->abort();
@@ -728,8 +1108,75 @@
728 1108 return $response;
729 1109 }
730 1110
731 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 + /**
732 1179 * Permission callback for route
733 1180 * @param \WP_REST_Request $wpRestRequest
734 1181 * @return mixed
735 1182 */
@@ -735,8 +1182,10 @@
735 1182 */
736 1183 public function permissionCallback($wpRestRequest)
737 1184 {
738 1185 try {
1186 + $this->parameters = null;
1187 + $this->substitutedParameters = null;
739 1188 $this->app->instance('route', $this);
740 1189 $this->app->instance('wprestrequest', $wpRestRequest);
741 1190 $this->app->request->mergeInputsFromRestRequest($wpRestRequest);
742 1191 $this->prepareCallbacks($this->app->request);
@@ -771,9 +1220,9 @@
771 1220 }
772 1221
773 1222 return $response;
774 1223
775 - } catch (Exception $e) {
1224 + } catch (Throwable $e) {
776 1225 return new WP_Error(
777 1226 'Permission Callback Error',
778 1227 $e->getMessage(), [
779 1228 'status' => $e->getCode() ?: 403
@@ -808,23 +1257,62 @@
808 1257 }
809 1258 }
810 1259
811 1260 /**
812 - * Dispatches the permission handler
1261 + * Dispatches the permission handler.
813 1262 *
814 1263 * @return bool|null
815 1264 */
816 1265 protected function dispatchPermissionHandler()
817 1266 {
818 - if ($this->permissionHandler) {
819 - return $this->app->call(
820 - $this->permissionHandler,
821 - $this->getControllerParameters()
822 - );
1267 + if (!$this->permissionHandler) {
1268 + return true;
823 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;
824 1289 }
825 1290
826 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 + /**
827 1315 * Gether route params after substituted the params
828 1316 *
829 1317 * @return array
830 1318 */
@@ -833,9 +1321,9 @@
833 1321 $routeParameters = [];
834 1322
835 1323 if (!$this->substitutedParameters) {
836 1324 if ($routeParameters = $this->getParameter()) {
837 - $routeParameters = $this->SubstituteParameters($routeParameters);
1325 + $routeParameters = $this->substituteParameters($routeParameters);
838 1326 }
839 1327 } else {
840 1328 $routeParameters = $this->substitutedParameters;
841 1329 }
@@ -860,9 +1348,11 @@
860 1348 * @return array
861 1349 */
862 1350 protected function collectMiddleWare($type = 'before')
863 1351 {
864 - $middleware = $this->app['config']->get('middleware', []);
1352 + $middleware = $this->app->bound('http.middleware')
1353 + ? $this->app['http.middleware']
1354 + : [];
865 1355
866 1356 $callableMiddleware = Arr::get($middleware, "global.{$type}", []);
867 1357
868 1358 $routeArray = [];
@@ -891,9 +1381,9 @@
891 1381 if (isset($handler)) {
892 1382 $this->addMiddlewareInTheStack($callableMiddleware, $handler);
893 1383 } else {
894 1384 if (isset($key)) {
895 - $mpath = 'config.middleware.route.' . $type;
1385 + $mpath = 'app/Http/middleware.php route.' . $type;
896 1386 $msg = "No middleware is assigned for the key: {$key} in {$mpath} array.";
897 1387 } else {
898 1388 $msg = "Could't resolve middleware.";
899 1389 }
@@ -912,9 +1402,8 @@
912 1402 * @return \Closure
913 1403 */
914 1404 protected function resolveMiddlewareFrom($class)
915 1405 {
916 - return (new $class);
917 1406 return static function ($r, $next, ...$params) use ($class) {
918 1407 return (new $class)->handle($r, $next, ...$params);
919 1408 };
920 1409 }
@@ -922,9 +1411,9 @@
922 1411 /**
923 1412 * Resolve the middleware
924 1413 *
925 1414 * @param mixed $handler
926 - * @param aray $pieces
1415 + * @param array $pieces
927 1416 * @return object
928 1417 */
929 1418 protected function resolveMiddleware($handler, $pieces)
930 1419 {
@@ -940,9 +1429,9 @@
940 1429 /**
941 1430 * Create a class to wrap the middleware
942 1431 *
943 1432 * @param mixed $handler
944 - * @param aray $pieces
1433 + * @param array $pieces
945 1434 * @return object
946 1435 */
947 1436 protected function wrapMiddleware($handler, $pieces)
948 1437 {
@@ -952,16 +1441,14 @@
952 1441
953 1442 return new class ($handler, $params) {
954 1443 protected $handler, $params = null;
955 1444
956 - public function __construct($handler, $params)
957 - {
1445 + public function __construct($handler, $params) {
958 1446 $this->handler = $handler;
959 1447 $this->params = $params;
960 1448 }
961 1449
962 - public function handle($r, $next)
963 - {
1450 + public function handle($r, $next) {
964 1451 if (is_callable($this->handler)) {
965 1452 return ($this->handler)($r, $next, ...$this->params);
966 1453 } else {
967 1454 if (!method_exists($this->handler, 'handle')) {
@@ -979,9 +1466,10 @@
979 1466 /**
980 1467 * Add the middleware in the stack
981 1468 *
982 1469 * @param array &$stack All callable middleware for the route
983 - * @param null
1470 + * @param string $middleware
1471 + * @return void
984 1472 */
985 1473 protected function addMiddlewareInTheStack(&$stack, $middleware)
986 1474 {
987 1475 if (!in_array($middleware, $stack)) {
@@ -1010,9 +1498,11 @@
1010 1498 if (function_exists($policyHandler)) {
1011 1499 return $policyHandler;
1012 1500 }
1013 1501
1014 - $policyHandlerFunction = substr($policyHandler, strrpos($policyHandler, '\\') + 1);
1502 + $policyHandlerFunction = substr(
1503 + $policyHandler, strrpos($policyHandler, '\\') + 1
1504 + );
1015 1505
1016 1506 if (function_exists($policyHandlerFunction)) {
1017 1507 return $policyHandlerFunction;
1018 1508 }
@@ -1025,15 +1515,12 @@
1025 1515 if (is_string($policyHandler) && $this->handler instanceof Closure) {
1026 1516
1027 1517 if (class_exists($policyHandler)) {
1028 1518
1029 - $reflection = new \ReflectionClass($policyHandler);
1519 + $reflection = new ReflectionClass($policyHandler);
1030 1520
1031 1521 if ($reflection->hasMethod('verifyRequest')) {
1032 -
1033 - $policyHandler = $policyHandler . '@' . 'verifyRequest';
1034 -
1035 - return $policyHandler;
1522 + return $policyHandler . '@' . 'verifyRequest';
1036 1523 }
1037 1524 } elseif (function_exists($policyHandler)) {
1038 1525 return $policyHandler;
1039 1526 }
@@ -1043,23 +1530,28 @@
1043 1530 );
1044 1531 }
1045 1532
1046 1533 if ($policyHandler && !function_exists($policyHandler)) {
1047 - if (is_string($this->handler) && strpos($this->handler, '@') !== false) {
1048 - list($_, $method) = explode('@', $this->handler);
1049 - $policyHandler = $policyHandler . '@' . $method;
1050 - } else if (is_array($this->handler)) {
1051 - $policyHandler = $policyHandler . '@' . $this->handler[1];
1052 - }
1534 + [$_, $method] = is_array($this->handler)
1535 + ? [$this->handler[0], $this->handler[1] ?? '__invoke']
1536 + : Str::parseCallback($this->handler, '__invoke');
1537 +
1538 + $policyHandler .= '@' . $method;
1053 1539 }
1054 1540
1055 1541 return $policyHandler ?: [$this, 'defaultPolicyHandler'];
1056 1542 }
1057 1543
1544 + /**
1545 + * Check if the policy handler is parseable.
1546 + *
1547 + * @param string $policyHandler
1548 + * @return boolean
1549 + */
1058 1550 protected function isPolicyHandlerParseable($policyHandler)
1059 1551 {
1060 - return (strpos($policyHandler, '@') === true
1061 - || strpos($policyHandler, '::') === true);
1552 + return (strpos($policyHandler, '@') !== false
1553 + || strpos($policyHandler, '::') !== false);
1062 1554 }
1063 1555
1064 1556 /**
1065 1557 * Default/Fallback policy handler for the route
@@ -1079,22 +1571,95 @@
1079 1571 * @throws \BadMethodCallException
1080 1572 */
1081 1573 public function prepareCallbacks($request)
1082 1574 {
1083 - $handler = $this->app->parseRestHandler(
1084 - $this->handler, $this->namespace
1085 - );
1575 + $handler = $this->app->parseRestHandler($this->handler, $this->namespace);
1086 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 + {
1087 1627 if ($handler instanceof Closure) {
1088 - $action = 'Closure';
1089 - $controller = null;
1090 - } else {
1091 - $handler = trim($handler, '\\');
1092 - $action = explode('@', $handler);
1093 - $pieces = explode('\\', $action[0]);
1094 - $controller = end($pieces);
1628 + return ['Closure', null];
1095 1629 }
1096 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 + {
1097 1662 try {
1098 1663 $policyHandler = $this->app->parsePolicyHandler(
1099 1664 $this->getPolicyHandler($this->policyHandler)
1100 1665 );
@@ -1101,16 +1666,13 @@
1101 1666
1102 1667 if ($policyHandler) {
1103 1668 $this->permissionHandler = $policyHandler;
1104 1669
1105 - // Adjust policy handler if the method was explicitly given
1106 - if (is_string($this->policyHandler)) {
1107 - if (is_array($policyHandler) && isset($policyHandler[1])) {
1108 - if ($pieces = explode('@', $this->policyHandler)) {
1109 - if (isset($pieces[1])) {
1110 - $this->permissionHandler[1] = $pieces[1];
1111 - }
1112 - }
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];
1113 1675 }
1114 1676 }
1115 1677
1116 1678 if (!is_callable($this->permissionHandler)) {
@@ -1118,46 +1680,37 @@
1118 1680 }
1119 1681 }
1120 1682
1121 1683 } catch (Exception $e) {
1122 - $pHandler = $this->policyHandler;
1123 - if (is_array($this->permissionHandler) && $this->permissionHandler) {
1124 - $pHandler = is_object($this->permissionHandler[0]) ?
1125 - get_class($this->permissionHandler[0]) . ':' . $this->permissionHandler[1] :
1126 - $this->permissionHandler[0] . ':' . $this->permissionHandler[1];
1127 - }
1128 -
1129 - throw new BadMethodCallException(
1130 - "The permission callback {$pHandler} is invalid or not callable."
1131 - );
1684 + throw $this->invalidPolicyHandlerException();
1132 1685 }
1133 1686
1134 - if (is_array($policyHandler)) {
1687 + // Convert object controller to class string for endpoint metadata
1688 + if (is_array($policyHandler) && is_object($policyHandler[0])) {
1135 1689 $policyHandler[0] = get_class($policyHandler[0]);
1136 1690 }
1137 1691
1138 - $this->actionInfo = [
1139 - 'handler' => is_object($handler) ? $action : $handler,
1140 - 'controller' => $controller,
1141 - 'method' => is_array($action) ? $action[1] : null,
1142 - 'path' => $this->uri,
1143 - 'http_method' => $request->get_method(),
1144 - 'full_uri' => $request->get_route(),
1145 - 'permission_callback' => $policyHandler,
1146 - 'compiled_url' => $this->compiled
1147 - ];
1692 + return $policyHandler;
1693 + }
1148 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;
1149 1703
1150 - $this->action = $handler;
1151 -
1152 - if ($routeParameters = $this->getParameter()) {
1153 - $this->substitutedParameters = $this->SubstituteParameters(
1154 - $routeParameters
1155 - );
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];
1156 1708 }
1157 1709
1158 -
1159 - return $this->action;
1710 + return new BadMethodCallException(
1711 + "The permission callback {$pHandler} is invalid or not callable."
1712 + );
1160 1713 }
1161 1714
1162 1715 /**
1163 1716 * Get one or more route parameters
@@ -1171,8 +1724,38 @@
1171 1724 $this->parameters = $this->app->request->get_url_params();
1172 1725 }
1173 1726
1174 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();
1175 1758 }
1176 1759
1177 1760 /**
1178 1761 * Dynamically access a route parameter.