PluginProbe
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration / 2.1.0
FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration v2.1.0
2.1.0 2.0.15 2.0.12 2.0.10 2.0.4 2.0.1 2.0.0 1.95.3 1.95.2 1.95 1.91.6 trunk 1.11 1.12 1.13 1.20 1.21 1.22 1.23 1.30 1.31 1.32 1.35 1.40 1.41 All 42 releases
← All changes | vendor/wpfluent/framework/src/WPFluent/Http/Route.php +621 -118 1.412.1.0 View file →
@@ -3,24 +3,29 @@
3 3 namespace FluentBoards\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 FluentBoards\Framework\Support\Arr;
15 +use FluentBoards\Framework\Support\Str;
13 16 use FluentBoards\Framework\Support\Pipeline;
14 17 use FluentBoards\Framework\Http\Request\Request;
18 +use FluentBoards\Framework\Http\Request\WPUserProxy;
19 +use FluentBoards\Framework\Http\SubstituteParameters;
15 20 use FluentBoards\Framework\Http\Middleware\RateLimiter;
16 21 use FluentBoards\Framework\Validator\ValidationException;
17 22 use FluentBoards\Framework\Database\Orm\ModelNotFoundException;
18 -use FluentBoards\Framework\Response\Response as WPFluentResponse;
23 +use FluentBoards\Framework\Http\Response\Response as WPFluentResponse;
19 24
20 25 class Route
21 26 {
22 - use SubstituteRouteParametersTrait;
27 + use SubstituteParameters;
23 28
24 29 /**
25 30 * Application Instance
26 31 * @var \FluentBoards\Framework\Foundation\Application
@@ -27,8 +32,14 @@
27 32 */
28 33 protected $app = null;
29 34
30 35 /**
36 + * Route name
37 + * @var string
38 + */
39 + protected $name = null;
40 +
41 + /**
31 42 * Rest namespace from config
32 43 * @var string
33 44 */
34 45 protected $restNamespace = null;
@@ -33,8 +44,14 @@
33 44 */
34 45 protected $restNamespace = null;
35 46
36 47 /**
48 + * Whether this route should override existing routes at the same URI.
49 + * @var bool
50 + */
51 + protected $shouldOverride = false;
52 +
53 + /**
37 54 * Full URI
38 55 * @var string
39 56 */
40 57 protected $uri = null;
@@ -152,8 +169,22 @@
152 169 */
153 170 protected $signed = false;
154 171
155 172 /**
173 + * Route signature.
174 + *
175 + * @var array
176 + */
177 + protected $endpointSignature = [];
178 +
179 + /**
180 + * Response instance
181 + *
182 + * @var \WP_REST_Response
183 + */
184 + protected $response = null;
185 +
186 + /**
156 187 * Construct the route instance
157 188 *
158 189 * @param \FluentBoards\Framework\Foundation\Application $app
159 190 * @param string $restNamespace
@@ -167,10 +198,8 @@
167 198 $this->restNamespace = $restNamespace;
168 199 $this->uri = $uri;
169 200 $this->handler = $handler;
170 201 $this->method = $method;
171 -
172 - $this->preparefrontendHandlers($handler);
173 202 }
174 203
175 204 /**
176 205 * Map the route to be used in front-end.
@@ -175,12 +204,14 @@
175 204 /**
176 205 * Map the route to be used in front-end.
177 206 *
178 207 * @param mixed $handler
179 - * @return null
208 + * @return self
180 209 */
181 - protected function preparefrontendHandlers($handler)
210 + public function preparefrontendHandlers()
182 211 {
212 + $handler = $this->handler;
213 +
183 214 $endpointsUrl = $this->app->config->get('app.slug') . '/__endpoints';
184 215
185 216 if (get_option('permalink_structure')) {
186 217 $url = $this->app->request->url();
@@ -187,37 +218,83 @@
187 218 } else {
188 219 $url = $this->app->request->query('rest_route');
189 220 }
190 221
191 - if (!str_contains($url ?? '', $endpointsUrl)) {
192 - return;
222 + if (
223 + !str_contains($url ?? '', $endpointsUrl)
224 + || $handler instanceof Closure
225 + ) {
226 + return $this;
193 227 }
194 228
195 - if ($handler instanceof Closure) {
196 - return;
197 - }
229 + [$controller, $cb] = Str::parseCallback($this->parseAction($handler));
198 230
199 - $action = trim($this->app->parseRestHandler($handler), '\\');
231 + $this->endpointSignature = [$controller, "_{$cb}"];
200 232
201 - [$controller, $cb] = explode('@', $action);
202 -
203 233 $controller = str_replace('\\', '.', $controller);
204 234
235 + // @phpstan-ignore-next-line
205 236 $endpoints = $this->app->endpoints;
206 237
207 238 $endpoints[$controller]["_{$cb}"] = [
208 239 'uri' => $this->uri,
209 - 'methods' => explode(',', $this->method)
240 + 'methods' => explode(',', $this->method),
241 + 'policy' => $this->getPolicyName()
210 242 ];
211 243
244 + // @phpstan-ignore-next-line
212 245 $this->app->endpoints = $endpoints;
246 +
247 + return $this;
213 248 }
214 249
215 250 /**
251 + * Get a display name for the route's policy handler.
252 + *
253 + * @return string|null
254 + */
255 + protected function getPolicyName()
256 + {
257 + if (!$this->policyHandler) {
258 + return null;
259 + }
260 +
261 + if ($this->policyHandler instanceof Closure) {
262 + return 'Closure';
263 + }
264 +
265 + $name = $this->policyHandler;
266 +
267 + if (is_string($name) && !$this->app->hasNamespace($name)) {
268 + $name = $this->app->__namespace__ . '\\App\\Http\\Policies\\' . $name;
269 + }
270 +
271 + return $name;
272 + }
273 +
274 + /**
275 + * Parse the action from the handler.
276 + *
277 + * @param mixed $handler
278 + * @return string
279 + */
280 + protected function parseAction($handler)
281 + {
282 + $action = $this->app->parseRestHandler($handler, $this->namespace);
283 + $action = trim($action, '\\');
284 +
285 + if (!str_contains($action, '@')) {
286 + $action .= '@__invoke';
287 + }
288 +
289 + return $action;
290 + }
291 +
292 + /**
216 293 * Alternative constructor
217 294 *
218 295 * @param \FluentBoards\Framework\Foundation\Application $app
219 - * @param string $restNamespace
296 + * @param string $namespace
220 297 * @param string $uri
221 298 * @param string $handler
222 299 * @param string $method
223 300 * @return self
@@ -421,8 +498,21 @@
421 498 return $this;
422 499 }
423 500
424 501 /**
502 + * Set the default route policy.
503 + *
504 + * @return self
505 + */
506 + public function withDefaultPolicy()
507 + {
508 + return $this->withPolicy(
509 + // @phpstan-ignore-next-line
510 + $this->app->__namespace__.'\\App\\Http\\Policies\\Policy'
511 + );
512 + }
513 +
514 + /**
425 515 * Set the route policy
426 516 *
427 517 * @param mixed $handler
428 518 * @param string|null $method
@@ -441,15 +531,83 @@
441 531 debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4)
442 532 );
443 533 }
444 534
535 + return $this->addRouteInfo($handler);
536 + }
537 +
538 + /**
539 + * Check if the request is from CLI;
540 + *
541 + * @return bool
542 + */
543 + protected function fromCli()
544 + {
545 + $hash = $this->app->request->header('X-From-CLI');
546 +
547 + $slugHash = md5($this->app->config->get('app.slug'));
548 +
549 + return $hash === $slugHash;
550 + }
551 +
552 + /**
553 + * Add route information for CLI command.
554 + *
555 + * @param mixed $handler
556 + * @return self
557 + */
558 + protected function addRouteInfo($handler)
559 + {
560 + if (!$this->fromCli()) {
561 + return $this;
562 + }
563 +
564 + if ($handler instanceof Closure) {
565 + $policyHandler = 'Closure';
566 + } else {
567 + $policyHandler = $this->resolvePolicyHandler();
568 + if (is_array($policyHandler)) {
569 + $policyHandler = implode('@', $policyHandler);
570 + }
571 + }
572 +
573 + $this->injectProp('policy', $policyHandler);
574 +
445 575 return $this;
446 576 }
447 577
448 578 /**
579 + * Inject property into route infio.
580 + *
581 + * @param string $key
582 + * @param mixed $value
583 + * @return void
584 + */
585 + public function injectProp($key, $value)
586 + {
587 + if (!$this->endpointSignature) {
588 + return;
589 + }
590 +
591 + [$controller, $cbKey] = $this->endpointSignature;
592 +
593 + $controllerKey = str_replace('\\', '.', $controller);
594 +
595 + // @phpstan-ignore-next-line
596 + $endpoints = $this->app->endpoints;
597 +
598 + if (isset($endpoints[$controllerKey][$cbKey])) {
599 + $endpoints[$controllerKey][$cbKey][$key] = $value;
600 + // @phpstan-ignore-next-line
601 + $this->app->endpoints = $endpoints;
602 + }
603 + }
604 +
605 + /**
449 606 * Resolve and set policy with namespace for add-ons
450 607 *
451 - * @param null
608 + * @param array $backTrace
609 + * @return void
452 610 */
453 611 protected function setPolicyHandlerWithNamespace($backTrace)
454 612 {
455 613 $last = end($backTrace);
@@ -468,15 +626,49 @@
468 626 }
469 627 }
470 628
471 629 /**
472 - * Set the namespace for controller/action
630 + * Set the name for the route.
631 + *
632 + * @param string $name
633 + * @return self
634 + */
635 + public function name($name)
636 + {
637 + if (!$this->name) {
638 + $this->name = $name;
639 + } else {
640 + $this->name .= $name;
641 + }
642 +
643 + // @phpstan-ignore-next-line
644 + return $this->app->router->setNamedRoute($this->name, $this);
645 + }
646 +
647 + /**
648 + * Set the name for the route.
649 + *
650 + * @param string $name
651 + * @return null
652 + */
653 + public function withName($name)
654 + {
655 + $this->name = implode('', $name);
656 + }
657 +
658 + /**
659 + * Set the namespace for controller/action.
660 + *
473 661 * @param string $ns
474 662 * @return null
475 663 */
476 664 public function withNamespace($ns)
477 665 {
478 - $this->namespace = implode('\\', $ns);
666 + if (is_array($ns)) {
667 + $this->namespace = implode('\\', $ns);
668 + } else {
669 + $this->namespace = trim($ns, '\\');
670 + }
479 671 }
480 672
481 673 /**
482 674 * Sign the route.
@@ -550,15 +742,48 @@
550 742 * @return null
551 743 */
552 744 public function register()
553 745 {
746 + $this->updateRouteOptions();
747 +
748 + return register_rest_route(
749 + $this->restNamespace,
750 + $this->getRouteUri(),
751 + $this->getOptions(),
752 + $this->shouldOverride
753 + );
754 + }
755 +
756 + /**
757 + * Update route options before registering.
758 + *
759 + * @return void
760 + */
761 + protected function updateRouteOptions()
762 + {
554 763 $this->setOptions();
764 + }
555 765
556 - $uri = '/' . trim($this->compileRoute($this->uri), '/');
766 + /**
767 + * Get normalized uri for the current route.
768 + *
769 + * @return string
770 + */
771 + protected function getRouteUri()
772 + {
773 + return '/' . trim($this->compileRoute($this->uri), '/');
774 + }
557 775
558 - return register_rest_route(
559 - $this->restNamespace, $uri, $this->getOptions()
560 - );
776 + /**
777 + * Mark this route to override any existing route at the same URI.
778 + *
779 + * @return $this
780 + */
781 + public function override()
782 + {
783 + $this->shouldOverride = true;
784 +
785 + return $this;
561 786 }
562 787
563 788 /**
564 789 * Set route options
@@ -566,9 +791,21 @@
566 791 * @return null
567 792 */
568 793 protected function setOptions()
569 794 {
570 - $this->options = [
795 + $this->options = array_merge(
796 + $this->options, $this->getDefaultOptions()
797 + );
798 + }
799 +
800 + /**
801 + * Get default options.
802 + *
803 + * @return array
804 + */
805 + protected function getDefaultOptions()
806 + {
807 + return [
571 808 [
572 809 'methods' => $this->method,
573 810 'callback' => [$this, 'callback'],
574 811 'permission_callback' => [$this, 'permissionCallback'],
@@ -573,9 +810,8 @@
573 810 'callback' => [$this, 'callback'],
574 811 'permission_callback' => [$this, 'permissionCallback'],
575 812 'args' => [],
576 813 ],
577 - 'schema' => [$this, 'getSchema'],
578 814 ];
579 815 }
580 816
581 817 /**
@@ -580,16 +816,16 @@
580 816
581 817 /**
582 818 * Generate and return the schema for the route.
583 819 *
584 - * @return \Closure
820 + * @return self
585 821 * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/schema
586 822 */
587 - public function getSchema()
823 + public function schema($schema)
588 824 {
589 - return function () {
590 - return [];
591 - };
825 + $this->options['schema'] = fn() => $schema;
826 +
827 + return $this;
592 828 }
593 829
594 830 /**
595 831 * Get item from predefined regex
@@ -654,17 +890,19 @@
654 890
655 891 /**
656 892 * Route handler
657 893 *
658 - * @return mixed
894 + * @return \WP_REST_Response
659 895 */
660 896 public function callback()
661 897 {
662 898 try {
663 - return $this->handleAfterMiddleware(
664 - $response = $this->dispatchRouteAction()
899 + $this->response = $this->handleAfterMiddleware(
900 + $this->dispatchRouteAction()
665 901 );
666 902
903 + return $this->handleResponse($this->response);
904 +
667 905 } catch (ValidationException $e) {
668 906 return $this->app->response->sendError(
669 907 $e->errors(), $e->getCode()
670 908 );
@@ -671,19 +909,111 @@
671 909 } catch (ModelNotFoundException $e) {
672 910 return $this->app->response->sendError([
673 911 'message' => $e->getMessage()
674 912 ], 404);
675 - } catch (Exception $e) {
676 - return $this->app->response->sendError([
677 - 'message' => $e->getMessage()
678 - ], $e->getCode() ?: 500);
913 + } catch (Throwable $e) {
914 + return $this->handleUnknownException(
915 + $e, $this->response ? $this->response->get_headers() : []
916 + );
679 917 }
680 918 }
681 919
682 920 /**
921 + * Handle response from route.
922 + *
923 + * @param \WP_REST_Response $response
924 + * @return \WP_REST_Response
925 + */
926 + protected function handleResponse($response)
927 + {
928 + if ($response->get_status() >= 400) {
929 + $this->fireExceptionEvent(
930 + new Exception(
931 + $this->extractErrorMessage($response),
932 + $response->get_status()
933 + )
934 + );
935 + }
936 +
937 + return $response;
938 + }
939 +
940 + /**
941 + * Extract error message from response data.
942 + *
943 + * @param \WP_REST_Response $response
944 + * @return string
945 + */
946 + protected function extractErrorMessage($response)
947 + {
948 + $data = $response->get_data();
949 +
950 + if (is_string($data)) {
951 + return $data;
952 + }
953 +
954 + if (is_array($data) && isset($data['message'])) {
955 + return $data['message'];
956 + }
957 +
958 + if ($data instanceof WP_Error) {
959 + return $data->get_error_message();
960 + }
961 +
962 + return 'Unknown error';
963 + }
964 +
965 + /**
966 + * Throw an exception based on the status code.
967 + *
968 + * @param string $message
969 + * @param int $status
970 + * @return null
971 + * @throws \Exception
972 + */
973 + protected function throwException($message, $status)
974 + {
975 + $class = sprintf(
976 + 'WpOrg\Requests\Exception\Http\Status%d', $status
977 + );
978 +
979 + if (!class_exists($class)) {
980 + $class = 'WpOrg\Requests\Exception\Http';
981 + }
982 +
983 + throw new $class($message, $status);
984 + }
985 +
986 + /**
987 + * Handle exception and send error response.
988 + *
989 + * @param Throwable $e
990 + * @return \WP_REST_Response
991 + */
992 + protected function handleUnknownException(Throwable $e, $headers = [])
993 + {
994 + $data = [];
995 +
996 + $this->fireExceptionEvent($e);
997 +
998 + if ($this->app->isDebugOn()) {
999 + $data = [
1000 + 'file' => $e->getFile(),
1001 + 'line' => $e->getLine(),
1002 + ];
1003 + }
1004 +
1005 + return $this->app->response->sendError([
1006 + 'code' => 'plugin_exception',
1007 + 'data' => $data,
1008 + 'message' => $e->getMessage(),
1009 + ], $e->getCode() ?: 500, $headers);
1010 + }
1011 +
1012 + /**
683 1013 * Dispatch the route action.
684 1014 *
685 - * @return mixed
1015 + * @return \WP_REST_Response
686 1016 */
687 1017 protected function dispatchRouteAction()
688 1018 {
689 1019 $response = $this->app->call(
@@ -710,15 +1040,12 @@
710 1040 protected function handleAfterMiddleware($response)
711 1041 {
712 1042 if (!$this->skipMiddleware) {
713 1043 $response = $this->app->make(Pipeline::class)
714 - ->send($response)
1044 + ->send(new WPFluentResponse($response))
715 1045 ->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;
1046 + ->then(function($response) {
1047 + return $this->normalize($response);
721 1048 });
722 1049
723 1050 if (!$response) {
724 1051 $response = $this->app->request->abort();
@@ -728,8 +1055,50 @@
728 1055 return $response;
729 1056 }
730 1057
731 1058 /**
1059 + * Normalize the response.
1060 + *
1061 + * @param mixed $response
1062 + * @return mixed
1063 + */
1064 + protected function normalize($response)
1065 + {
1066 + if ($response instanceof WPFluentResponse) {
1067 + $response = $response->toArray();
1068 + }
1069 +
1070 + if (!$response instanceof WP_REST_Response) {
1071 + return new WP_REST_Response($response);
1072 + }
1073 +
1074 + return $response;
1075 + }
1076 +
1077 + /**
1078 + * Fire exception action hook.
1079 + *
1080 + * @param Exception $exception
1081 + * @return void
1082 + */
1083 + protected function fireExceptionEvent($exception)
1084 + {
1085 + if ($this->app->isDebugOn() || defined('FLUENT_BRIDGE_SECRET')) {
1086 + $message = sprintf(
1087 + "%s in %s:%d\nStack trace:\n%s\n",
1088 + $exception->getMessage(),
1089 + $exception->getFile(),
1090 + $exception->getLine(),
1091 + $exception->getTraceAsString()
1092 + );
1093 +
1094 + error_log($message);
1095 +
1096 + $this->app->doAction('fluent_exception', $exception);
1097 + }
1098 + }
1099 +
1100 + /**
732 1101 * Permission callback for route
733 1102 * @param \WP_REST_Request $wpRestRequest
734 1103 * @return mixed
735 1104 */
@@ -735,8 +1104,10 @@
735 1104 */
736 1105 public function permissionCallback($wpRestRequest)
737 1106 {
738 1107 try {
1108 + $this->parameters = null;
1109 + $this->substitutedParameters = null;
739 1110 $this->app->instance('route', $this);
740 1111 $this->app->instance('wprestrequest', $wpRestRequest);
741 1112 $this->app->request->mergeInputsFromRestRequest($wpRestRequest);
742 1113 $this->prepareCallbacks($this->app->request);
@@ -808,23 +1179,62 @@
808 1179 }
809 1180 }
810 1181
811 1182 /**
812 - * Dispatches the permission handler
1183 + * Dispatches the permission handler.
813 1184 *
814 1185 * @return bool|null
815 1186 */
816 1187 protected function dispatchPermissionHandler()
817 1188 {
818 - if ($this->permissionHandler) {
819 - return $this->app->call(
820 - $this->permissionHandler,
821 - $this->getControllerParameters()
822 - );
1189 + if (!$this->permissionHandler) {
1190 + return true;
823 1191 }
1192 +
1193 + $isValid = $this->app->call(
1194 + $this->permissionHandler,
1195 + $this->getControllerParameters()
1196 + );
1197 +
1198 + if (is_object($isValid)) {
1199 + if ($this->isUser($isValid)) {
1200 + $isValid = $isValid->id();
1201 + } else {
1202 + $this->throwInvalidPolicy();
1203 + }
1204 + }
1205 +
1206 + if (!is_bool($isValid) && !is_int($isValid) && !is_null($isValid)) {
1207 + $this->throwInvalidPolicy();
1208 + }
1209 +
1210 + return (bool) $isValid;
824 1211 }
825 1212
826 1213 /**
1214 + * Checks if the user is an instance of WPUserProxy.
1215 + *
1216 + * @param WPUserProxy $user
1217 + * @return bool
1218 + */
1219 + protected function isUser($user)
1220 + {
1221 + return $user instanceof WPUserProxy;
1222 + }
1223 +
1224 + /**
1225 + * Throw invalid policy handling exception.
1226 + *
1227 + * @return InvalidArgumentException
1228 + */
1229 + protected function throwInvalidPolicy()
1230 + {
1231 + throw new InvalidArgumentException(
1232 + 'The policy must return a boolean, integer, null, or a WPUserProxy instance.', 500
1233 + );
1234 + }
1235 +
1236 + /**
827 1237 * Gether route params after substituted the params
828 1238 *
829 1239 * @return array
830 1240 */
@@ -833,9 +1243,9 @@
833 1243 $routeParameters = [];
834 1244
835 1245 if (!$this->substitutedParameters) {
836 1246 if ($routeParameters = $this->getParameter()) {
837 - $routeParameters = $this->SubstituteParameters($routeParameters);
1247 + $routeParameters = $this->substituteParameters($routeParameters);
838 1248 }
839 1249 } else {
840 1250 $routeParameters = $this->substitutedParameters;
841 1251 }
@@ -912,9 +1322,8 @@
912 1322 * @return \Closure
913 1323 */
914 1324 protected function resolveMiddlewareFrom($class)
915 1325 {
916 - return (new $class);
917 1326 return static function ($r, $next, ...$params) use ($class) {
918 1327 return (new $class)->handle($r, $next, ...$params);
919 1328 };
920 1329 }
@@ -922,9 +1331,9 @@
922 1331 /**
923 1332 * Resolve the middleware
924 1333 *
925 1334 * @param mixed $handler
926 - * @param aray $pieces
1335 + * @param array $pieces
927 1336 * @return object
928 1337 */
929 1338 protected function resolveMiddleware($handler, $pieces)
930 1339 {
@@ -940,9 +1349,9 @@
940 1349 /**
941 1350 * Create a class to wrap the middleware
942 1351 *
943 1352 * @param mixed $handler
944 - * @param aray $pieces
1353 + * @param array $pieces
945 1354 * @return object
946 1355 */
947 1356 protected function wrapMiddleware($handler, $pieces)
948 1357 {
@@ -952,16 +1361,14 @@
952 1361
953 1362 return new class ($handler, $params) {
954 1363 protected $handler, $params = null;
955 1364
956 - public function __construct($handler, $params)
957 - {
1365 + public function __construct($handler, $params) {
958 1366 $this->handler = $handler;
959 1367 $this->params = $params;
960 1368 }
961 1369
962 - public function handle($r, $next)
963 - {
1370 + public function handle($r, $next) {
964 1371 if (is_callable($this->handler)) {
965 1372 return ($this->handler)($r, $next, ...$this->params);
966 1373 } else {
967 1374 if (!method_exists($this->handler, 'handle')) {
@@ -979,9 +1386,10 @@
979 1386 /**
980 1387 * Add the middleware in the stack
981 1388 *
982 1389 * @param array &$stack All callable middleware for the route
983 - * @param null
1390 + * @param string $middleware
1391 + * @return void
984 1392 */
985 1393 protected function addMiddlewareInTheStack(&$stack, $middleware)
986 1394 {
987 1395 if (!in_array($middleware, $stack)) {
@@ -1010,9 +1418,11 @@
1010 1418 if (function_exists($policyHandler)) {
1011 1419 return $policyHandler;
1012 1420 }
1013 1421
1014 - $policyHandlerFunction = substr($policyHandler, strrpos($policyHandler, '\\') + 1);
1422 + $policyHandlerFunction = substr(
1423 + $policyHandler, strrpos($policyHandler, '\\') + 1
1424 + );
1015 1425
1016 1426 if (function_exists($policyHandlerFunction)) {
1017 1427 return $policyHandlerFunction;
1018 1428 }
@@ -1025,15 +1435,12 @@
1025 1435 if (is_string($policyHandler) && $this->handler instanceof Closure) {
1026 1436
1027 1437 if (class_exists($policyHandler)) {
1028 1438
1029 - $reflection = new \ReflectionClass($policyHandler);
1439 + $reflection = new ReflectionClass($policyHandler);
1030 1440
1031 1441 if ($reflection->hasMethod('verifyRequest')) {
1032 -
1033 - $policyHandler = $policyHandler . '@' . 'verifyRequest';
1034 -
1035 - return $policyHandler;
1442 + return $policyHandler . '@' . 'verifyRequest';
1036 1443 }
1037 1444 } elseif (function_exists($policyHandler)) {
1038 1445 return $policyHandler;
1039 1446 }
@@ -1043,19 +1450,24 @@
1043 1450 );
1044 1451 }
1045 1452
1046 1453 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 - }
1454 + [$_, $method] = is_array($this->handler)
1455 + ? [$this->handler[0], $this->handler[1] ?? '__invoke']
1456 + : Str::parseCallback($this->handler, '__invoke');
1457 +
1458 + $policyHandler .= '@' . $method;
1053 1459 }
1054 1460
1055 1461 return $policyHandler ?: [$this, 'defaultPolicyHandler'];
1056 1462 }
1057 1463
1464 + /**
1465 + * Check if the policy handler is parseable.
1466 + *
1467 + * @param string $policyHandler
1468 + * @return boolean
1469 + */
1058 1470 protected function isPolicyHandlerParseable($policyHandler)
1059 1471 {
1060 1472 return (strpos($policyHandler, '@') === true
1061 1473 || strpos($policyHandler, '::') === true);
@@ -1079,22 +1491,95 @@
1079 1491 * @throws \BadMethodCallException
1080 1492 */
1081 1493 public function prepareCallbacks($request)
1082 1494 {
1083 - $handler = $this->app->parseRestHandler(
1084 - $this->handler, $this->namespace
1085 - );
1495 + $handler = $this->app->parseRestHandler($this->handler, $this->namespace);
1086 1496
1497 + [$action, $controller] = $this->resolveHandlerDetails($handler);
1498 +
1499 + $policyHandler = $this->resolvePolicyHandler();
1500 +
1501 + $this->actionInfo = [
1502 + 'handler' => is_object($handler) ? $action : $handler,
1503 + 'controller' => $controller,
1504 + 'method' => $this->getMethodName($action, $handler),
1505 + 'path' => $this->uri,
1506 + 'http_method' => $request->get_method(),
1507 + 'full_uri' => $request->get_route(),
1508 + 'permission_callback' => $policyHandler,
1509 + 'compiled_url' => $this->compiled
1510 + ];
1511 +
1512 + $this->action = $handler;
1513 +
1514 + if ($routeParameters = $this->getParameter()) {
1515 + $this->substitutedParameters = $this->substituteParameters($routeParameters);
1516 + }
1517 +
1518 + return $this->action;
1519 + }
1520 +
1521 + /**
1522 + * Get the method name to build action info.
1523 + *
1524 + * @param mixed $action
1525 + * @param mixed $handler
1526 + * @return string|null
1527 + */
1528 + protected function getMethodName($action, $handler)
1529 + {
1530 + $method = is_array($action) ? $action[1] ?? '__invoke' : null;
1531 +
1532 + if (is_null($method) && is_object($handler)) {
1533 + $method = '__invoke';
1534 + }
1535 +
1536 + return $method;
1537 + }
1538 +
1539 + /**
1540 + * Resolve the handler details.
1541 + *
1542 + * @param mixed $handler
1543 + * @return array
1544 + */
1545 + protected function resolveHandlerDetails($handler)
1546 + {
1087 1547 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);
1548 + return ['Closure', null];
1095 1549 }
1096 1550
1551 + if (is_object($handler)) {
1552 + $class = get_class($handler);
1553 + return [$class, $class];
1554 + }
1555 +
1556 + $handler = trim($handler, '\\');
1557 + [$controller, $method] = Str::parseCallback($handler, '__invoke');
1558 + $controllerName = $this->extractControllerName($controller);
1559 +
1560 + return [[$controller, $method], $controllerName];
1561 + }
1562 +
1563 + /**
1564 + * Extract the controller name from the FQCN.
1565 + *
1566 + * @param string $fqcn
1567 + * @return string
1568 + */
1569 + protected function extractControllerName($fqcn)
1570 + {
1571 + $parts = explode('\\', $fqcn);
1572 + return end($parts);
1573 + }
1574 +
1575 + /**
1576 + * Parse and validate the policy handler.
1577 + *
1578 + * @return array
1579 + */
1580 + protected function resolvePolicyHandler()
1581 + {
1097 1582 try {
1098 1583 $policyHandler = $this->app->parsePolicyHandler(
1099 1584 $this->getPolicyHandler($this->policyHandler)
1100 1585 );
@@ -1101,16 +1586,13 @@
1101 1586
1102 1587 if ($policyHandler) {
1103 1588 $this->permissionHandler = $policyHandler;
1104 1589
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 - }
1590 + // Adjust method if explicitly given in string policy handler
1591 + if (is_string($this->policyHandler) && is_array($policyHandler) && isset($policyHandler[1])) {
1592 + $pieces = explode('@', $this->policyHandler);
1593 + if (isset($pieces[1])) {
1594 + $this->permissionHandler[1] = $pieces[1];
1113 1595 }
1114 1596 }
1115 1597
1116 1598 if (!is_callable($this->permissionHandler)) {
@@ -1118,46 +1600,37 @@
1118 1600 }
1119 1601 }
1120 1602
1121 1603 } 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 - );
1604 + throw $this->invalidPolicyHandlerException();
1132 1605 }
1133 1606
1134 - if (is_array($policyHandler)) {
1607 + // Convert object controller to class string for endpoint metadata
1608 + if (is_array($policyHandler) && is_object($policyHandler[0])) {
1135 1609 $policyHandler[0] = get_class($policyHandler[0]);
1136 1610 }
1137 1611
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 - ];
1612 + return $policyHandler;
1613 + }
1148 1614
1615 + /**
1616 + * Build and throw an exception for invalid policy handlers.
1617 + *
1618 + * @throws \BadMethodCallException
1619 + */
1620 + protected function invalidPolicyHandlerException()
1621 + {
1622 + $pHandler = $this->policyHandler;
1149 1623
1150 - $this->action = $handler;
1151 -
1152 - if ($routeParameters = $this->getParameter()) {
1153 - $this->substitutedParameters = $this->SubstituteParameters(
1154 - $routeParameters
1155 - );
1624 + if (is_array($this->permissionHandler) && $this->permissionHandler) {
1625 + $pHandler = is_object($this->permissionHandler[0])
1626 + ? get_class($this->permissionHandler[0]) . ':' . $this->permissionHandler[1]
1627 + : $this->permissionHandler[0] . ':' . $this->permissionHandler[1];
1156 1628 }
1157 1629
1158 -
1159 - return $this->action;
1630 + return new BadMethodCallException(
1631 + "The permission callback {$pHandler} is invalid or not callable."
1632 + );
1160 1633 }
1161 1634
1162 1635 /**
1163 1636 * Get one or more route parameters
@@ -1171,8 +1644,38 @@
1171 1644 $this->parameters = $this->app->request->get_url_params();
1172 1645 }
1173 1646
1174 1647 return $key ? $this->parameters[$key] : $this->parameters;
1648 + }
1649 +
1650 + /**
1651 + * Get the name of the route.
1652 + *
1653 + * @return string
1654 + */
1655 + public function getName()
1656 + {
1657 + return $this->name;
1658 + }
1659 +
1660 + /**
1661 + * Get the url of the route.
1662 + *
1663 + * @return string
1664 + */
1665 + public function getUrl()
1666 + {
1667 + return $this->uri;
1668 + }
1669 +
1670 + /**
1671 + * Get the url of the route.
1672 + *
1673 + * @return string
1674 + */
1675 + public function uri()
1676 + {
1677 + return $this->getUrl();
1175 1678 }
1176 1679
1177 1680 /**
1178 1681 * Dynamically access a route parameter.