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 +1015 -231 1.7.0 → 2.5.0 View file →
@@ -3,22 +3,32 @@
3 3 namespace FluentBooking\Framework\Http;
4 4
5 5 use Closure;
6 6 use Exception;
7 +use Throwable;
8 +use WP_Error;
7 9 use WP_REST_Request;
8 10 use WP_REST_Response;
11 +use ReflectionClass;
9 12 use BadMethodCallException;
10 13 use InvalidArgumentException;
11 14 use FluentBooking\Framework\Support\Arr;
15 +use FluentBooking\Framework\Support\Str;
12 16 use FluentBooking\Framework\Support\Pipeline;
17 +use FluentBooking\Framework\Http\Request\Request;
18 +use FluentBooking\Framework\Http\Request\WPUserProxy;
19 +use FluentBooking\Framework\Http\SubstituteParameters;
20 +use FluentBooking\Framework\Http\Middleware\RateLimiter;
13 21 use FluentBooking\Framework\Validator\ValidationException;
14 22 use FluentBooking\Framework\Database\Orm\ModelNotFoundException;
15 -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;
16 26
17 27 class Route
18 28 {
19 - use SubstituteRouteParametersTrait;
20 -
29 + use SubstituteParameters;
30 +
21 31 /**
22 32 * Application Instance
23 33 * @var \FluentBooking\Framework\Foundation\Application
24 34 */
@@ -24,8 +34,14 @@
24 34 */
25 35 protected $app = null;
26 36
27 37 /**
38 + * Route name
39 + * @var string
40 + */
41 + protected $name = null;
42 +
43 + /**
28 44 * Rest namespace from config
29 45 * @var string
30 46 */
31 47 protected $restNamespace = null;
@@ -30,13 +46,19 @@
30 46 */
31 47 protected $restNamespace = null;
32 48
33 49 /**
50 + * Whether this route should override existing routes at the same URI.
51 + * @var bool
52 + */
53 + protected $shouldOverride = false;
54 +
55 + /**
34 56 * Full URI
35 57 * @var string
36 58 */
37 59 protected $uri = null;
38 -
60 +
39 61 /**
40 62 * Compiled rest endpoint
41 63 * @var string
42 64 */
@@ -52,9 +74,9 @@
52 74 * Rest Handler/Callback before parsing
53 75 * @var string
54 76 */
55 77 protected $handler = null;
56 -
78 +
57 79 /**
58 80 * Rest Handler/Callback after parsing
59 81 * @var callable|string
60 82 */
@@ -64,9 +86,9 @@
64 86 * Rest route action info after parsing
65 87 * @var array
66 88 */
67 89 protected $actionInfo = [];
68 -
90 +
69 91 /**
70 92 * Policy Handler/Callback after parsing
71 93 * @var string
72 94 */
@@ -76,9 +98,9 @@
76 98 * HTTP Methods
77 99 * @var string
78 100 */
79 101 protected $method = null;
80 -
102 +
81 103 /**
82 104 * Rest options
83 105 * @var array
84 106 */
@@ -94,9 +116,9 @@
94 116 * Rest namespace
95 117 * @var string
96 118 */
97 119 protected $namespace = null;
98 -
120 +
99 121 /**
100 122 * Policy Handler/Callback after parsing
101 123 * @var callable|string
102 124 */
@@ -112,9 +134,9 @@
112 134 ];
113 135
114 136 /**
115 137 * Skips middlewar if true
116 - *
138 + *
117 139 * @var boolean
118 140 */
119 141 protected $skipMiddleware = false;
120 142
@@ -136,16 +158,37 @@
136 158 protected $parameters = null;
137 159
138 160 /**
139 161 * Route substituted parameters
140 - *
162 + *
141 163 * @var null|array
142 164 */
143 165 protected $substitutedParameters = [];
144 166
145 167 /**
168 + * Is route signed
169 + *
170 + * @var boolean
171 + */
172 + protected $signed = false;
173 +
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 + /**
146 189 * Construct the route instance
147 - *
190 + *
148 191 * @param \FluentBooking\Framework\Foundation\Application $app
149 192 * @param string $restNamespace
150 193 * @param string $uri
151 194 * @param string $handler
@@ -160,12 +203,99 @@
160 203 $this->method = $method;
161 204 }
162 205
163 206 /**
207 + * Map the route to be used in front-end.
208 + *
209 + * @return self
210 + */
211 + public function preparefrontendHandlers()
212 + {
213 + $handler = $this->handler;
214 +
215 + $endpointsUrl = $this->app->config->get('app.slug') . '/__endpoints';
216 +
217 + if (get_option('permalink_structure')) {
218 + $url = $this->app->request->url();
219 + } else {
220 + $url = $this->app->request->query('rest_route');
221 + }
222 +
223 + if (
224 + !str_contains($url ?? '', $endpointsUrl)
225 + || $handler instanceof Closure
226 + ) {
227 + return $this;
228 + }
229 +
230 + [$controller, $cb] = Str::parseCallback($this->parseAction($handler));
231 +
232 + $this->endpointSignature = [$controller, "_{$cb}"];
233 +
234 + $controller = str_replace('\\', '.', $controller);
235 +
236 + // @phpstan-ignore-next-line
237 + $endpoints = $this->app->endpoints;
238 +
239 + $endpoints[$controller]["_{$cb}"] = [
240 + 'uri' => $this->uri,
241 + 'methods' => explode(',', $this->method),
242 + 'policy' => $this->getPolicyName()
243 + ];
244 +
245 + // @phpstan-ignore-next-line
246 + $this->app->endpoints = $endpoints;
247 +
248 + return $this;
249 + }
250 +
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 + /**
164 294 * Alternative constructor
165 - *
295 + *
166 296 * @param \FluentBooking\Framework\Foundation\Application $app
167 - * @param string $restNamespace
297 + * @param string $namespace
168 298 * @param string $uri
169 299 * @param string $handler
170 300 * @param string $method
171 301 * @return self
@@ -176,11 +306,11 @@
176 306 }
177 307
178 308 /**
179 309 * Set route meta
180 - *
181 - * @param string $key
182 - * @param mixed $value
310 + *
311 + * @param string $key
312 + * @param mixed $value
183 313 * @return self
184 314 */
185 315 public function meta($key, $value = null)
186 316 {
@@ -192,10 +322,10 @@
192 322 }
193 323
194 324 /**
195 325 * Get route meta
196 - *
197 - * @param string $key
326 + *
327 + * @param string $key
198 328 * @return mixed
199 329 */
200 330 public function getMeta($key = '')
201 331 {
@@ -201,15 +331,15 @@
201 331 {
202 332 if (isset($this->meta[$key])) {
203 333 return $this->meta[$key];
204 334 }
205 -
335 +
206 336 return $this->meta;
207 337 }
208 338
209 339 /**
210 340 * Get route options
211 - *
341 + *
212 342 * @return mixed
213 343 */
214 344 public function getOptions()
215 345 {
@@ -217,10 +347,10 @@
217 347 }
218 348
219 349 /**
220 350 * Get route options
221 - *
222 - * @param string $key
351 + *
352 + * @param string $key
223 353 * @return mixed
224 354 */
225 355 public function getOption($key = null)
226 356 {
@@ -228,9 +358,9 @@
228 358 }
229 359
230 360 /**
231 361 * Get route action information
232 - * @param string $key
362 + * @param string $key
233 363 * @return mixed
234 364 */
235 365 public function getAction($key = '')
236 366 {
@@ -236,17 +366,17 @@
236 366 {
237 367 if ($key && array_key_exists($key, $this->actionInfo)) {
238 368 return $this->actionInfo[$key];
239 369 }
240 -
370 +
241 371 return $this->actionInfo;
242 372 }
243 373
244 374 /**
245 375 * Set a where constrain into the route
246 - *
247 - * @param string $identifier
248 - * @param string $value
376 + *
377 + * @param string $identifier
378 + * @param string $value
249 379 * @return self
250 380 */
251 381 public function where($identifier, $value = null)
252 382 {
@@ -262,10 +392,10 @@
262 392 }
263 393
264 394 /**
265 395 * Add an integer type route constraint
266 - *
267 - * @param string $identifiers
396 + *
397 + * @param string $identifiers
268 398 * @return self
269 399 */
270 400 public function int($identifiers)
271 401 {
@@ -279,10 +409,10 @@
279 409 }
280 410
281 411 /**
282 412 * Add an alpha type route constraint
283 - *
284 - * @param string $identifiers
413 + *
414 + * @param string $identifiers
285 415 * @return self
286 416 */
287 417 public function alpha($identifiers)
288 418 {
@@ -296,10 +426,10 @@
296 426 }
297 427
298 428 /**
299 429 * Add an alphanum type route constraint
300 - *
301 - * @param string $identifiers
430 + *
431 + * @param string $identifiers
302 432 * @return self
303 433 */
304 434 public function alphaNum($identifiers)
305 435 {
@@ -313,10 +443,10 @@
313 443 }
314 444
315 445 /**
316 446 * Add an alphanumdash type route constraint
317 - *
318 - * @param string $identifiers
447 + *
448 + * @param string $identifiers
319 449 * @return self
320 450 */
321 451 public function alphaNumDash($identifiers)
322 452 {
@@ -330,10 +460,10 @@
330 460 }
331 461
332 462 /**
333 463 * Set the route before middleware
334 - *
335 - * @param array|string $middleware
464 + *
465 + * @param array|string $middleware
336 466 * @return self
337 467 */
338 468 public function before(...$middleware)
339 469 {
@@ -341,10 +471,10 @@
341 471 }
342 472
343 473 /**
344 474 * Set the route after middleware
345 - *
346 - * @param array|string $middleware
475 + *
476 + * @param array|string $middleware
347 477 * @return self
348 478 */
349 479 public function after(...$middleware)
350 480 {
@@ -352,9 +482,9 @@
352 482 }
353 483
354 484 /**
355 485 * Set the route middleware
356 - * @param array $middleware
486 + * @param array $middleware
357 487 * @return self
358 488 */
359 489 public function middleware($type = 'before', ...$middleware)
360 490 {
@@ -369,12 +499,25 @@
369 499 return $this;
370 500 }
371 501
372 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 + /**
373 516 * Set the route policy
374 - *
375 - * @param mixed $handler
376 - * @param string|null $method
517 + *
518 + * @param mixed $handler
519 + * @param string|null $method
377 520 * @return self
378 521 */
379 522 public function withPolicy($handler, $method = null)
380 523 {
@@ -389,15 +532,83 @@
389 532 debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4)
390 533 );
391 534 }
392 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 +
393 576 return $this;
394 577 }
395 578
396 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 + /**
397 607 * Resolve and set policy with namespace for add-ons
398 - *
399 - * @param null
608 + *
609 + * @param array $backTrace
610 + * @return void
400 611 */
401 612 protected function setPolicyHandlerWithNamespace($backTrace)
402 613 {
403 614 $last = end($backTrace);
@@ -406,9 +617,9 @@
406 617
407 618 $class = $last['class'];
408 619
409 620 $namespace = substr(__NAMESPACE__, 0, strpos(__NAMESPACE__, '\\'));
410 -
621 +
411 622 $calledClassNamespace = substr($class, 0, strpos($class, '\\'));
412 623
413 624 if ($namespace != $calledClassNamespace) {
414 625 $ns = $calledClassNamespace . '\\App\\Http\\Policies\\';
@@ -416,51 +627,211 @@
416 627 }
417 628 }
418 629
419 630 /**
420 - * Set the namespace for controller/action
421 - * @param string $ns
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
422 652 * @return null
423 653 */
654 + public function withName($name)
655 + {
656 + $this->name = implode('', $name);
657 + }
658 +
659 + /**
660 + * Set the namespace for controller/action.
661 + *
662 + * @param string $ns
663 + * @return null
664 + */
424 665 public function withNamespace($ns)
425 666 {
426 - $this->namespace = implode('\\', $ns);
667 + if (is_array($ns)) {
668 + $this->namespace = implode('\\', $ns);
669 + } else {
670 + $this->namespace = trim($ns, '\\');
671 + }
427 672 }
428 673
429 674 /**
675 + * Sign the route.
676 + *
677 + * @return $this
678 + */
679 + public function signed()
680 + {
681 + $this->signed = true;
682 +
683 + return $this;
684 + }
685 +
686 + /**
687 + * Apply rate limit to the route.
688 + *
689 + * @param int $limit Number of allowed requests.
690 + * @param int $interval Time interval in seconds.
691 + *
692 + * @return $this
693 + */
694 + public function rateLimit($limit, $interval)
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 +
700 + $rateLimiter = new RateLimiter($limit * 2, $interval);
701 +
702 + $this->middleware('before', $rateLimiter);
703 +
704 + return $this;
705 + }
706 +
707 + /**
708 + * Apply a rate limit to the route for per minute.
709 + *
710 + * @param int $limit The maximum number of requests allowed per minute.
711 + * @return $this
712 + */
713 + public function rateLimitPerMinute($limit)
714 + {
715 + return $this->rateLimit($limit, MINUTE_IN_SECONDS);
716 + }
717 +
718 + /**
719 + * Apply an hourly rate limit to the route.
720 + *
721 + * @param int $limit The maximum number of requests allowed per hour.
722 + * @return $this
723 + */
724 + public function rateLimitHourly($limit)
725 + {
726 + return $this->rateLimit($limit, HOUR_IN_SECONDS);
727 + }
728 +
729 + /**
730 + * Apply a daily basis (24 hours) rate limit to the route.
731 + *
732 + * @param int $limit The maximum number of requests allowed per day.
733 + * @return $this
734 + */
735 + public function rateLimitDaily($limit)
736 + {
737 + return $this->rateLimit($limit, DAY_IN_SECONDS);
738 + }
739 +
740 + /**
430 741 * Register the rest endpoint
431 - *
742 + *
432 743 * @return null
433 744 */
434 745 public function register()
435 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 + {
436 764 $this->setOptions();
765 + }
437 766
438 - $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 + }
439 776
440 - return register_rest_route(
441 - $this->restNamespace, $uri, $this->getOptions()
442 - );
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;
443 787 }
444 788
445 789 /**
446 790 * Set route options
447 - *
791 + *
448 792 * @return null
449 793 */
450 794 protected function setOptions()
451 795 {
452 - $this->options = [
453 - 'args' => [],
454 - 'methods' => $this->method,
455 - 'callback' => [$this, 'callback'],
456 - 'permission_callback' => [$this, 'permissionCallback']
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 [
809 + [
810 + 'methods' => $this->method,
811 + 'callback' => [$this, 'callback'],
812 + 'permission_callback' => [$this, 'permissionCallback'],
813 + 'args' => [],
814 + ],
457 815 ];
458 816 }
459 817
460 818 /**
819 + * Generate and return the schema for the route.
820 + *
821 + * @return self
822 + * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/schema
823 + */
824 + public function schema($schema)
825 + {
826 + $this->options['schema'] = fn() => $schema;
827 +
828 + return $this;
829 + }
830 +
831 + /**
461 832 * Get item from predefined regex
462 - * @param string $value
833 + * @param string $value
463 834 * @return string
464 835 */
465 836 protected function getValue($value)
466 837 {
@@ -472,10 +843,10 @@
472 843 }
473 844
474 845 /**
475 846 * Compikle the rest route to regex
476 - *
477 - * @param string $uri
847 + *
848 + * @param string $uri
478 849 * @return string compiled rest endpoint
479 850 */
480 851 protected function compileRoute($uri)
481 852 {
@@ -480,12 +851,12 @@
480 851 protected function compileRoute($uri)
481 852 {
482 853 $params = [];
483 854
484 - $compiledUri = preg_replace_callback('#/{(.*?)}#', function($match) use (&$params, $uri) {
855 + $compiledUri = preg_replace_callback('#/{(.*?)}#', function ($match) use (&$params, $uri) {
485 856 // Default regx
486 857 $regx = '[^\s(?!/)]+';
487 -
858 +
488 859 $param = trim($match[1]);
489 860
490 861 if ($isOptional = strpos($param, '?')) {
491 862 $param = trim($param, '?');
@@ -495,9 +866,9 @@
495 866 throw new InvalidArgumentException(
496 867 "Duplicate parameter name '{$param}' found in {$uri}.", 500
497 868 );
498 869 }
499 -
870 +
500 871 $params[] = $param;
501 872
502 873 if (isset($this->wheres[$param])) {
503 874 $regx = $this->wheres[$param];
@@ -507,11 +878,11 @@
507 878
508 879 if ($isOptional) {
509 880 $pattern = "(?:" . $pattern . ")?";
510 881 }
511 -
882 +
512 883 $this->options['args'][$param]['required'] = !$isOptional;
513 -
884 +
514 885 return $pattern;
515 886
516 887 }, $uri);
517 888
@@ -519,89 +890,318 @@
519 890 }
520 891
521 892 /**
522 893 * Route handler
523 - *
524 - * @return mixed
894 + *
895 + * @return \WP_REST_Response
525 896 */
526 897 public function callback()
527 898 {
528 899 try {
900 + $this->response = $this->handleAfterMiddleware(
901 + $this->dispatchRouteAction()
902 + );
529 903
530 - $response = $this->app->call(
531 - $this->action, $this->getControllerParameters()
904 + return $this->handleResponse($this->response);
905 +
906 + } catch (ValidationException $e) {
907 + return $this->app->response->sendError(
908 + $e->errors(), $e->getCode()
532 909 );
910 + } catch (ModelNotFoundException $e) {
911 + return $this->app->response->sendError([
912 + 'message' => $e->getMessage()
913 + ], 404);
914 + } catch (HttpException $e) {
915 + return $this->renderHttpException($e);
916 + } catch (Throwable $e) {
917 + $headers = $this->response ? $this->response->get_headers() : [];
533 918
534 - if ($response instanceof WPFluentResponse) {
535 - $response = $response->toArray();
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;
536 928 }
537 929
538 - if (!($response instanceof WP_REST_Response)) {
539 - if (is_wp_error($response)) {
540 - $response = $this->app->response->wpErrorToResponse($response);
541 - } else {
542 - $response = $this->app->response->sendSuccess($response);
543 - }
930 + return $this->handleUnknownException($e, $headers);
931 + }
932 + }
933 +
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 + /**
1066 + * Dispatch the route action.
1067 + *
1068 + * @return \WP_REST_Response
1069 + */
1070 + protected function dispatchRouteAction()
1071 + {
1072 + $response = $this->app->call(
1073 + $this->action, $this->getControllerParameters()
1074 + );
1075 +
1076 + if ($response instanceof WPFluentResponse) {
1077 + $response = $response->toArray();
1078 + } elseif (!($response instanceof WP_REST_Response)) {
1079 + $response = !is_wp_error($response) ?
1080 + $this->app->response->sendSuccess($response) :
1081 + $this->app->response->wpErrorToResponse($response);
1082 + }
1083 +
1084 + return $response;
1085 + }
1086 +
1087 + /**
1088 + * Handle after middleware if any.
1089 + *
1090 + * @param mixed $response
1091 + * @return mixed
1092 + */
1093 + protected function handleAfterMiddleware($response)
1094 + {
1095 + if (!$this->skipMiddleware) {
1096 + $response = $this->app->make(Pipeline::class)
1097 + ->send(new WPFluentResponse($response))
1098 + ->through($this->collectMiddleWare('after'))
1099 + ->then(function($response) {
1100 + return $this->normalize($response);
1101 + });
1102 +
1103 + if (!$response) {
1104 + $response = $this->app->request->abort();
544 1105 }
545 -
546 - if (!$this->skipMiddleware) {
547 - $response = $this->app->make(Pipeline::class)
548 - ->send($response)
549 - ->through($this->collectMiddleWare('after'))
550 - ->then(function($response) {
551 - if (!$response instanceof WP_REST_Response) {
552 - $response = new WP_REST_Response($response);
553 - }
554 - return $response;
555 - });
1106 + }
556 1107
557 - if (!$response) {
558 - $response = $this->app->request->abort();
559 - }
560 - }
1108 + return $response;
1109 + }
561 1110
562 - return $response;
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 + }
563 1122
564 - } catch (ValidationException $e) {
565 - return $this->app->response->sendError(
566 - $e->errors(), $e->getCode()
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()
567 1154 );
568 - } catch (ModelNotFoundException $e) {
569 - return $this->app->response->sendError([
570 - 'message' => $e->getMessage()
571 - ], 404);
572 - } catch (Exception $e) {
573 - return $this->app->response->sendError([
574 - 'message' => $e->getMessage()
575 - ], $e->getCode() ?: 500);
1155 +
1156 + error_log($message);
576 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 + }
577 1176 }
578 1177
579 1178 /**
580 1179 * Permission callback for route
581 - * @param \WP_REST_Request $wpRestRequest
1180 + * @param \WP_REST_Request $wpRestRequest
582 1181 * @return mixed
583 1182 */
584 1183 public function permissionCallback($wpRestRequest)
585 1184 {
586 1185 try {
587 -
1186 + $this->parameters = null;
1187 + $this->substitutedParameters = null;
588 1188 $this->app->instance('route', $this);
589 -
590 - if (!$this->app->bound('wprestrequest')) {
591 - $this->app->instance('wprestrequest', $wpRestRequest);
592 - $this->app->request->mergeInputsFromRestRequest($wpRestRequest);
1189 + $this->app->instance('wprestrequest', $wpRestRequest);
1190 + $this->app->request->mergeInputsFromRestRequest($wpRestRequest);
1191 + $this->prepareCallbacks($this->app->request);
593 1192
594 - if (method_exists($this, 'prepareCallbacks')) {
595 - $this->prepareCallbacks($this->app->request);
596 - }
1193 + if (!$this->isThisValidSignedRoute()) {
1194 + throw new Exception('Invalid Signature', 403);
597 1195 }
598 1196
599 1197 $response = $this->app->make(Pipeline::class)
600 1198 ->send($this->app->request)
601 1199 ->through($this->collectMiddleWare('before'))
602 - ->then(function($request) {
603 - return $this->dispatchPermissionHandler();
1200 + ->then(function ($request) {
1201 + if ($request && $request instanceof Request) {
1202 + return $this->dispatchPermissionHandler();
1203 + }
604 1204 });
605 1205
606 1206 if (is_wp_error($response)) {
607 1207 throw new Exception(
@@ -613,42 +1213,108 @@
613 1213 if ($response instanceof WP_REST_Response) {
614 1214 $data = $response->get_data();
615 1215
616 1216 throw new Exception(
617 - $data['message'] ?? $response->get_status(), $response->get_status()
1217 + $data['message'] ?? $response->get_status(),
1218 + $response->get_status()
618 1219 );
619 1220 }
620 1221
621 1222 return $response;
622 1223
623 - } catch (Exception $e) {
624 - $this->skipMiddleware = true;
625 - $this->action = function() use ($e) {
626 - return $this->app->response->sendError(
627 - ['message' => $e->getMessage()], $e->getCode()
628 - );
629 - };
1224 + } catch (Throwable $e) {
1225 + return new WP_Error(
1226 + 'Permission Callback Error',
1227 + $e->getMessage(), [
1228 + 'status' => $e->getCode() ?: 403
1229 + ]
1230 + );
630 1231 }
631 1232 }
632 1233
633 1234 /**
634 - * Dispatches the permission handler
1235 + * Checks if the route is signed and needs validation.
635 1236 *
1237 + * @return boolean [description]
1238 + */
1239 + protected function isThisValidSignedRoute()
1240 + {
1241 + if (!$this->signed) return true;
1242 +
1243 + $request = $this->app->make('request');
1244 +
1245 + if ($this->app->make('url')->validate($request->getFullUrl())) {
1246 + parse_str($this->app->make('encrypter')->decrypt(
1247 + $this->app->request->get('_data')
1248 + ), $query);
1249 +
1250 + $this->app->request->merge(
1251 + Arr::except($query, ['expires_at'])
1252 + );
1253 +
1254 + $this->app->request->forget('_data');
1255 +
1256 + return true;
1257 + }
1258 + }
1259 +
1260 + /**
1261 + * Dispatches the permission handler.
1262 + *
636 1263 * @return bool|null
637 1264 */
638 1265 protected function dispatchPermissionHandler()
639 1266 {
640 - if ($this->permissionHandler) {
641 - return $this->app->call(
642 - $this->permissionHandler,
643 - $this->getControllerParameters()
644 - );
1267 + if (!$this->permissionHandler) {
1268 + return true;
645 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;
646 1289 }
647 1290
648 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 + /**
649 1315 * Gether route params after substituted the params
650 - *
1316 + *
651 1317 * @return array
652 1318 */
653 1319 protected function getControllerParameters()
654 1320 {
@@ -655,9 +1321,9 @@
655 1321 $routeParameters = [];
656 1322
657 1323 if (!$this->substitutedParameters) {
658 1324 if ($routeParameters = $this->getParameter()) {
659 - $routeParameters = $this->SubstituteParameters($routeParameters);
1325 + $routeParameters = $this->substituteParameters($routeParameters);
660 1326 }
661 1327 } else {
662 1328 $routeParameters = $this->substitutedParameters;
663 1329 }
@@ -669,9 +1335,9 @@
669 1335 * Added the ability to add middleware so we can intercept
670 1336 * the request without modifying the source code again
671 1337 * and again. The middleware class will implement
672 1338 * the handle method as given below:
673 - *
1339 + *
674 1340 * public function handle($request, $next)
675 1341 *
676 1342 * And must return $next($request) to handle the request.
677 1343 * Otherwise return nothing to abort the request.
@@ -676,15 +1342,17 @@
676 1342 * And must return $next($request) to handle the request.
677 1343 * Otherwise return nothing to abort the request.
678 1344 * Optionally, you may call the abort method:
679 1345 * return $request->abort(code, message);
680 - *
1346 + *
681 1347 * @param string $type
682 1348 * @return array
683 1349 */
684 1350 protected function collectMiddleWare($type = 'before')
685 1351 {
686 - $middleware = $this->app['config']->get('middleware', []);
1352 + $middleware = $this->app->bound('http.middleware')
1353 + ? $this->app['http.middleware']
1354 + : [];
687 1355
688 1356 $callableMiddleware = Arr::get($middleware, "global.{$type}", []);
689 1357
690 1358 $routeArray = [];
@@ -699,12 +1367,13 @@
699 1367 foreach ($this->middleware[$type] as $routeMiddleware) {
700 1368
701 1369 if (is_object($routeMiddleware)) {
702 1370 $handler = $routeMiddleware;
1371 + } elseif (class_exists($routeMiddleware)) {
1372 + $handler = $this->resolveMiddlewareFrom($routeMiddleware);
703 1373 } else {
704 1374 $pieces = explode(':', $routeMiddleware);
705 1375 $handler = Arr::get($routeArray, $key = reset($pieces));
706 -
707 1376 if (isset($pieces[1])) {
708 1377 $handler = $this->resolveMiddleware($handler, $pieces);
709 1378 }
710 1379 }
@@ -712,14 +1381,14 @@
712 1381 if (isset($handler)) {
713 1382 $this->addMiddlewareInTheStack($callableMiddleware, $handler);
714 1383 } else {
715 1384 if (isset($key)) {
716 - $mpath = 'config.middleware.route.' . $type;
1385 + $mpath = 'app/Http/middleware.php route.' . $type;
717 1386 $msg = "No middleware is assigned for the key: {$key} in {$mpath} array.";
718 1387 } else {
719 1388 $msg = "Could't resolve middleware.";
720 1389 }
721 -
1390 +
722 1391 throw new InvalidArgumentException($msg);
723 1392 }
724 1393 }
725 1394
@@ -726,12 +1395,25 @@
726 1395 return $callableMiddleware;
727 1396 }
728 1397
729 1398 /**
1399 + * Resolve a middleware from a class.
1400 + *
1401 + * @param string $class
1402 + * @return \Closure
1403 + */
1404 + protected function resolveMiddlewareFrom($class)
1405 + {
1406 + return static function ($r, $next, ...$params) use ($class) {
1407 + return (new $class)->handle($r, $next, ...$params);
1408 + };
1409 + }
1410 +
1411 + /**
730 1412 * Resolve the middleware
731 - *
732 - * @param mixed $handler
733 - * @param aray $pieces
1413 + *
1414 + * @param mixed $handler
1415 + * @param array $pieces
734 1416 * @return object
735 1417 */
736 1418 protected function resolveMiddleware($handler, $pieces)
737 1419 {
@@ -739,38 +1421,44 @@
739 1421 $handler = $this->wrapMiddleware($handler, $pieces);
740 1422 } elseif (is_string($handler)) {
741 1423 $handler = $handler . ':' . str_replace(' ', '', end($pieces));
742 1424 }
743 -
1425 +
744 1426 return $handler;
745 1427 }
746 1428
747 1429 /**
748 1430 * Create a class to wrap the middleware
749 - *
750 - * @param mixed $handler
751 - * @param aray $pieces
1431 + *
1432 + * @param mixed $handler
1433 + * @param array $pieces
752 1434 * @return object
753 1435 */
754 1436 protected function wrapMiddleware($handler, $pieces)
755 1437 {
756 1438 $params = str_replace(' ', '', end($pieces));
757 -
1439 +
758 1440 $params = explode(',', $params);
759 1441
760 1442 return new class ($handler, $params) {
761 1443 protected $handler, $params = null;
1444 +
762 1445 public function __construct($handler, $params) {
763 1446 $this->handler = $handler;
764 1447 $this->params = $params;
765 1448 }
766 - public function handle($target, $next) {
1449 +
1450 + public function handle($r, $next) {
767 1451 if (is_callable($this->handler)) {
768 - return ($this->handler)($target, $next, ...$this->params);
1452 + return ($this->handler)($r, $next, ...$this->params);
769 1453 } else {
770 - return $this->handler->handle(
771 - $target, $next, ...$this->params
772 - );
1454 + if (!method_exists($this->handler, 'handle')) {
1455 + $class = get_class($this->handler);
1456 + throw new InvalidArgumentException(
1457 + "The {$class} must implement the handle method."
1458 + );
1459 + }
1460 + return $this->handler->handle($r, $next, ...$this->params);
773 1461 }
774 1462 }
775 1463 };
776 1464 }
@@ -776,11 +1464,12 @@
776 1464 }
777 1465
778 1466 /**
779 1467 * Add the middleware in the stack
780 - *
1468 + *
781 1469 * @param array &$stack All callable middleware for the route
782 - * @param null
1470 + * @param string $middleware
1471 + * @return void
783 1472 */
784 1473 protected function addMiddlewareInTheStack(&$stack, $middleware)
785 1474 {
786 1475 if (!in_array($middleware, $stack)) {
@@ -789,10 +1478,10 @@
789 1478 }
790 1479
791 1480 /**
792 1481 * Resolve the policy handler
793 - *
794 - * @param string $policyHandler
1482 + *
1483 + * @param string $policyHandler
795 1484 * @return mixed
796 1485 */
797 1486 protected function getPolicyHandler($policyHandler)
798 1487 {
@@ -804,15 +1493,17 @@
804 1493 return $policyHandler;
805 1494 }
806 1495
807 1496 if (is_string($policyHandler)) {
808 -
1497 +
809 1498 if (function_exists($policyHandler)) {
810 1499 return $policyHandler;
811 1500 }
812 1501
813 - $policyHandlerFunction = substr($policyHandler, strrpos($policyHandler, '\\') + 1);
814 -
1502 + $policyHandlerFunction = substr(
1503 + $policyHandler, strrpos($policyHandler, '\\') + 1
1504 + );
1505 +
815 1506 if (function_exists($policyHandlerFunction)) {
816 1507 return $policyHandlerFunction;
817 1508 }
818 1509 }
@@ -823,16 +1514,13 @@
823 1514
824 1515 if (is_string($policyHandler) && $this->handler instanceof Closure) {
825 1516
826 1517 if (class_exists($policyHandler)) {
827 -
828 - $reflection = new \ReflectionClass($policyHandler);
829 -
1518 +
1519 + $reflection = new ReflectionClass($policyHandler);
1520 +
830 1521 if ($reflection->hasMethod('verifyRequest')) {
831 -
832 - $policyHandler = $policyHandler . '@' . 'verifyRequest';
833 -
834 - return $policyHandler;
1522 + return $policyHandler . '@' . 'verifyRequest';
835 1523 }
836 1524 } elseif (function_exists($policyHandler)) {
837 1525 return $policyHandler;
838 1526 }
@@ -840,30 +1528,35 @@
840 1528 throw new InvalidArgumentException(
841 1529 'Explicit policy handler is required while using a closure as route callback.'
842 1530 );
843 1531 }
844 -
1532 +
845 1533 if ($policyHandler && !function_exists($policyHandler)) {
846 - if (is_string($this->handler) && strpos($this->handler, '@') !== false) {
847 - list($_, $method) = explode('@', $this->handler);
848 - $policyHandler = $policyHandler . '@' . $method;
849 - } else if (is_array($this->handler)) {
850 - $policyHandler = $policyHandler . '@' . $this->handler[1];
851 - }
1534 + [$_, $method] = is_array($this->handler)
1535 + ? [$this->handler[0], $this->handler[1] ?? '__invoke']
1536 + : Str::parseCallback($this->handler, '__invoke');
1537 +
1538 + $policyHandler .= '@' . $method;
852 1539 }
853 1540
854 1541 return $policyHandler ?: [$this, 'defaultPolicyHandler'];
855 1542 }
856 1543
1544 + /**
1545 + * Check if the policy handler is parseable.
1546 + *
1547 + * @param string $policyHandler
1548 + * @return boolean
1549 + */
857 1550 protected function isPolicyHandlerParseable($policyHandler)
858 1551 {
859 - return (strpos($policyHandler, '@') === true
860 - || strpos($policyHandler, '::') === true);
1552 + return (strpos($policyHandler, '@') !== false
1553 + || strpos($policyHandler, '::') !== false);
861 1554 }
862 1555
863 1556 /**
864 1557 * Default/Fallback policy handler for the route
865 - *
1558 + *
866 1559 * @return bool
867 1560 */
868 1561 public function defaultPolicyHandler()
869 1562 {
@@ -871,45 +1564,115 @@
871 1564 }
872 1565
873 1566 /**
874 1567 * Parse the rest and permission/policy handlers
875 - *
876 - * @param \WP_REST_Request $request
1568 + *
1569 + * @param \WP_REST_Request $request
877 1570 * @return null
878 1571 * @throws \BadMethodCallException
879 1572 */
880 1573 public function prepareCallbacks($request)
881 1574 {
882 - $handler = $this->app->parseRestHandler(
883 - $this->handler, $this->namespace
884 - );
1575 + $handler = $this->app->parseRestHandler($this->handler, $this->namespace);
885 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 + {
886 1627 if ($handler instanceof Closure) {
887 - $action = 'Closure';
888 - $controller = null;
889 - } else {
890 - $handler = trim($handler, '\\');
891 - $action = explode('@', $handler);
892 - $pieces = explode('\\', $action[0]);
893 - $controller = end($pieces);
1628 + return ['Closure', null];
894 1629 }
895 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 + {
896 1662 try {
897 1663 $policyHandler = $this->app->parsePolicyHandler(
898 1664 $this->getPolicyHandler($this->policyHandler)
899 1665 );
900 -
1666 +
901 1667 if ($policyHandler) {
902 1668 $this->permissionHandler = $policyHandler;
903 1669
904 - // Adjust policy handler if the method was explicitly given
905 - if (is_string($this->policyHandler)) {
906 - if (is_array($policyHandler) && isset($policyHandler[1])) {
907 - if ($pieces = explode('@', $this->policyHandler)) {
908 - if (isset($pieces[1])) {
909 - $this->permissionHandler[1] = $pieces[1];
910 - }
911 - }
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];
912 1675 }
913 1676 }
914 1677
915 1678 if (!is_callable($this->permissionHandler)) {
@@ -917,52 +1680,43 @@
917 1680 }
918 1681 }
919 1682
920 1683 } catch (Exception $e) {
921 - $pHandler = $this->policyHandler;
922 - if (is_array($this->permissionHandler) && $this->permissionHandler) {
923 - $pHandler = is_object($this->permissionHandler[0]) ?
924 - get_class($this->permissionHandler[0]) . ':' . $this->permissionHandler[1] :
925 - $this->permissionHandler[0] . ':' . $this->permissionHandler[1];
926 - }
927 -
928 - throw new BadMethodCallException(
929 - "The permission callback {$pHandler} is invalid or not callable."
930 - );
1684 + throw $this->invalidPolicyHandlerException();
931 1685 }
932 1686
933 - if (is_array($policyHandler)) {
1687 + // Convert object controller to class string for endpoint metadata
1688 + if (is_array($policyHandler) && is_object($policyHandler[0])) {
934 1689 $policyHandler[0] = get_class($policyHandler[0]);
935 1690 }
936 1691
937 - $this->actionInfo = [
938 - 'handler' => is_object($handler) ? $action : $handler,
939 - 'controller' => $controller,
940 - 'method' => is_array($action) ? $action[1] : null,
941 - 'path' => $this->uri,
942 - 'http_method' => $request->get_method(),
943 - 'full_uri' => $request->get_route(),
944 - 'permission_callback' => $policyHandler,
945 - 'compiled_url' => $this->compiled
946 - ];
1692 + return $policyHandler;
1693 + }
947 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;
948 1703
949 - $this->action = $handler;
950 -
951 - if ($routeParameters = $this->getParameter()) {
952 - $this->substitutedParameters = $this->SubstituteParameters(
953 - $routeParameters
954 - );
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];
955 1708 }
956 1709
957 -
958 - return $this->action;
1710 + return new BadMethodCallException(
1711 + "The permission callback {$pHandler} is invalid or not callable."
1712 + );
959 1713 }
960 1714
961 1715 /**
962 - * Get route one or more parameters
963 - * @param string $key
964 - *
1716 + * Get one or more route parameters
1717 + * @param string $key
1718 + *
965 1719 * @return mixed
966 1720 */
967 1721 public function getParameter($key = null)
968 1722 {
@@ -973,10 +1727,40 @@
973 1727 return $key ? $this->parameters[$key] : $this->parameters;
974 1728 }
975 1729
976 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();
1758 + }
1759 +
1760 + /**
977 1761 * Dynamically access a route parameter.
978 - *
1762 + *
979 1763 * @param string $key
980 1764 * @return mixed
981 1765 */
982 1766 public function __get($key)