PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.11
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.11
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / vendor / wpfluent / framework / src / WPFluent / Http / Route.php

Route.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.11, at vendor/wpfluent/framework/src/WPFluent/Http/Route.php

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