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
fluent-boards / vendor / wpfluent / framework / src / WPFluent / Http / Route.php

Route.php in FluentBoards – Project Management, Task Management, Goal Tracking, Kanban Board, and, Team Collaboration 2.1.0, at vendor/wpfluent/framework/src/WPFluent/Http/Route.php

1,691 lines 41.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBoards\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 FluentBoards\Framework\Support\Arr;
15 use FluentBoards\Framework\Support\Str;
16 use FluentBoards\Framework\Support\Pipeline;
17 use FluentBoards\Framework\Http\Request\Request;
18 use FluentBoards\Framework\Http\Request\WPUserProxy;
19 use FluentBoards\Framework\Http\SubstituteParameters;
20 use FluentBoards\Framework\Http\Middleware\RateLimiter;
21 use FluentBoards\Framework\Validator\ValidationException;
22 use FluentBoards\Framework\Database\Orm\ModelNotFoundException;
23 use FluentBoards\Framework\Http\Response\Response as WPFluentResponse;
24
25 class Route
26 {
27 use SubstituteParameters;
28
29 /**
30 * Application Instance
31 * @var \FluentBoards\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 \FluentBoards\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 \FluentBoards\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 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 /**
1013 * Dispatch the route action.
1014 *
1015 * @return \WP_REST_Response
1016 */
1017 protected function dispatchRouteAction()
1018 {
1019 $response = $this->app->call(
1020 $this->action, $this->getControllerParameters()
1021 );
1022
1023 if ($response instanceof WPFluentResponse) {
1024 $response = $response->toArray();
1025 } elseif (!($response instanceof WP_REST_Response)) {
1026 $response = !is_wp_error($response) ?
1027 $this->app->response->sendSuccess($response) :
1028 $this->app->response->wpErrorToResponse($response);
1029 }
1030
1031 return $response;
1032 }
1033
1034 /**
1035 * Handle after middleware if any.
1036 *
1037 * @param mixed $response
1038 * @return mixed
1039 */
1040 protected function handleAfterMiddleware($response)
1041 {
1042 if (!$this->skipMiddleware) {
1043 $response = $this->app->make(Pipeline::class)
1044 ->send(new WPFluentResponse($response))
1045 ->through($this->collectMiddleWare('after'))
1046 ->then(function($response) {
1047 return $this->normalize($response);
1048 });
1049
1050 if (!$response) {
1051 $response = $this->app->request->abort();
1052 }
1053 }
1054
1055 return $response;
1056 }
1057
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 /**
1101 * Permission callback for route
1102 * @param \WP_REST_Request $wpRestRequest
1103 * @return mixed
1104 */
1105 public function permissionCallback($wpRestRequest)
1106 {
1107 try {
1108 $this->parameters = null;
1109 $this->substitutedParameters = null;
1110 $this->app->instance('route', $this);
1111 $this->app->instance('wprestrequest', $wpRestRequest);
1112 $this->app->request->mergeInputsFromRestRequest($wpRestRequest);
1113 $this->prepareCallbacks($this->app->request);
1114
1115 if (!$this->isThisValidSignedRoute()) {
1116 throw new Exception('Invalid Signature', 403);
1117 }
1118
1119 $response = $this->app->make(Pipeline::class)
1120 ->send($this->app->request)
1121 ->through($this->collectMiddleWare('before'))
1122 ->then(function ($request) {
1123 if ($request && $request instanceof Request) {
1124 return $this->dispatchPermissionHandler();
1125 }
1126 });
1127
1128 if (is_wp_error($response)) {
1129 throw new Exception(
1130 $response->get_error_message(),
1131 is_int($code = $response->get_error_code()) ? $code : 403
1132 );
1133 }
1134
1135 if ($response instanceof WP_REST_Response) {
1136 $data = $response->get_data();
1137
1138 throw new Exception(
1139 $data['message'] ?? $response->get_status(),
1140 $response->get_status()
1141 );
1142 }
1143
1144 return $response;
1145
1146 } catch (Exception $e) {
1147 return new WP_Error(
1148 'Permission Callback Error',
1149 $e->getMessage(), [
1150 'status' => $e->getCode() ?: 403
1151 ]
1152 );
1153 }
1154 }
1155
1156 /**
1157 * Checks if the route is signed and needs validation.
1158 *
1159 * @return boolean [description]
1160 */
1161 protected function isThisValidSignedRoute()
1162 {
1163 if (!$this->signed) return true;
1164
1165 $request = $this->app->make('request');
1166
1167 if ($this->app->make('url')->validate($request->getFullUrl())) {
1168 parse_str($this->app->make('encrypter')->decrypt(
1169 $this->app->request->get('_data')
1170 ), $query);
1171
1172 $this->app->request->merge(
1173 Arr::except($query, ['expires_at'])
1174 );
1175
1176 $this->app->request->forget('_data');
1177
1178 return true;
1179 }
1180 }
1181
1182 /**
1183 * Dispatches the permission handler.
1184 *
1185 * @return bool|null
1186 */
1187 protected function dispatchPermissionHandler()
1188 {
1189 if (!$this->permissionHandler) {
1190 return true;
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;
1211 }
1212
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 /**
1237 * Gether route params after substituted the params
1238 *
1239 * @return array
1240 */
1241 protected function getControllerParameters()
1242 {
1243 $routeParameters = [];
1244
1245 if (!$this->substitutedParameters) {
1246 if ($routeParameters = $this->getParameter()) {
1247 $routeParameters = $this->substituteParameters($routeParameters);
1248 }
1249 } else {
1250 $routeParameters = $this->substitutedParameters;
1251 }
1252
1253 return $routeParameters;
1254 }
1255
1256 /**
1257 * Added the ability to add middleware so we can intercept
1258 * the request without modifying the source code again
1259 * and again. The middleware class will implement
1260 * the handle method as given below:
1261 *
1262 * public function handle($request, $next)
1263 *
1264 * And must return $next($request) to handle the request.
1265 * Otherwise return nothing to abort the request.
1266 * Optionally, you may call the abort method:
1267 * return $request->abort(code, message);
1268 *
1269 * @param string $type
1270 * @return array
1271 */
1272 protected function collectMiddleWare($type = 'before')
1273 {
1274 $middleware = $this->app['config']->get('middleware', []);
1275
1276 $callableMiddleware = Arr::get($middleware, "global.{$type}", []);
1277
1278 $routeArray = [];
1279
1280 if (isset($middleware['route'])) {
1281 $routeArray = $middleware['route'];
1282 if (isset($routeArray[$type])) {
1283 $routeArray = $routeArray[$type];
1284 }
1285 }
1286
1287 foreach ($this->middleware[$type] as $routeMiddleware) {
1288
1289 if (is_object($routeMiddleware)) {
1290 $handler = $routeMiddleware;
1291 } elseif (class_exists($routeMiddleware)) {
1292 $handler = $this->resolveMiddlewareFrom($routeMiddleware);
1293 } else {
1294 $pieces = explode(':', $routeMiddleware);
1295 $handler = Arr::get($routeArray, $key = reset($pieces));
1296 if (isset($pieces[1])) {
1297 $handler = $this->resolveMiddleware($handler, $pieces);
1298 }
1299 }
1300
1301 if (isset($handler)) {
1302 $this->addMiddlewareInTheStack($callableMiddleware, $handler);
1303 } else {
1304 if (isset($key)) {
1305 $mpath = 'config.middleware.route.' . $type;
1306 $msg = "No middleware is assigned for the key: {$key} in {$mpath} array.";
1307 } else {
1308 $msg = "Could't resolve middleware.";
1309 }
1310
1311 throw new InvalidArgumentException($msg);
1312 }
1313 }
1314
1315 return $callableMiddleware;
1316 }
1317
1318 /**
1319 * Resolve a middleware from a class.
1320 *
1321 * @param string $class
1322 * @return \Closure
1323 */
1324 protected function resolveMiddlewareFrom($class)
1325 {
1326 return static function ($r, $next, ...$params) use ($class) {
1327 return (new $class)->handle($r, $next, ...$params);
1328 };
1329 }
1330
1331 /**
1332 * Resolve the middleware
1333 *
1334 * @param mixed $handler
1335 * @param array $pieces
1336 * @return object
1337 */
1338 protected function resolveMiddleware($handler, $pieces)
1339 {
1340 if (is_object($handler)) {
1341 $handler = $this->wrapMiddleware($handler, $pieces);
1342 } elseif (is_string($handler)) {
1343 $handler = $handler . ':' . str_replace(' ', '', end($pieces));
1344 }
1345
1346 return $handler;
1347 }
1348
1349 /**
1350 * Create a class to wrap the middleware
1351 *
1352 * @param mixed $handler
1353 * @param array $pieces
1354 * @return object
1355 */
1356 protected function wrapMiddleware($handler, $pieces)
1357 {
1358 $params = str_replace(' ', '', end($pieces));
1359
1360 $params = explode(',', $params);
1361
1362 return new class ($handler, $params) {
1363 protected $handler, $params = null;
1364
1365 public function __construct($handler, $params) {
1366 $this->handler = $handler;
1367 $this->params = $params;
1368 }
1369
1370 public function handle($r, $next) {
1371 if (is_callable($this->handler)) {
1372 return ($this->handler)($r, $next, ...$this->params);
1373 } else {
1374 if (!method_exists($this->handler, 'handle')) {
1375 $class = get_class($this->handler);
1376 throw new InvalidArgumentException(
1377 "The {$class} must implement the handle method."
1378 );
1379 }
1380 return $this->handler->handle($r, $next, ...$this->params);
1381 }
1382 }
1383 };
1384 }
1385
1386 /**
1387 * Add the middleware in the stack
1388 *
1389 * @param array &$stack All callable middleware for the route
1390 * @param string $middleware
1391 * @return void
1392 */
1393 protected function addMiddlewareInTheStack(&$stack, $middleware)
1394 {
1395 if (!in_array($middleware, $stack)) {
1396 $stack[] = $middleware;
1397 }
1398 }
1399
1400 /**
1401 * Resolve the policy handler
1402 *
1403 * @param string $policyHandler
1404 * @return mixed
1405 */
1406 protected function getPolicyHandler($policyHandler)
1407 {
1408 if (!$policyHandler) {
1409 return [$this, 'defaultPolicyHandler'];
1410 }
1411
1412 if (is_callable($policyHandler)) {
1413 return $policyHandler;
1414 }
1415
1416 if (is_string($policyHandler)) {
1417
1418 if (function_exists($policyHandler)) {
1419 return $policyHandler;
1420 }
1421
1422 $policyHandlerFunction = substr(
1423 $policyHandler, strrpos($policyHandler, '\\') + 1
1424 );
1425
1426 if (function_exists($policyHandlerFunction)) {
1427 return $policyHandlerFunction;
1428 }
1429 }
1430
1431 if ($this->isPolicyHandlerParseable($policyHandler)) {
1432 return $policyHandler;
1433 }
1434
1435 if (is_string($policyHandler) && $this->handler instanceof Closure) {
1436
1437 if (class_exists($policyHandler)) {
1438
1439 $reflection = new ReflectionClass($policyHandler);
1440
1441 if ($reflection->hasMethod('verifyRequest')) {
1442 return $policyHandler . '@' . 'verifyRequest';
1443 }
1444 } elseif (function_exists($policyHandler)) {
1445 return $policyHandler;
1446 }
1447
1448 throw new InvalidArgumentException(
1449 'Explicit policy handler is required while using a closure as route callback.'
1450 );
1451 }
1452
1453 if ($policyHandler && !function_exists($policyHandler)) {
1454 [$_, $method] = is_array($this->handler)
1455 ? [$this->handler[0], $this->handler[1] ?? '__invoke']
1456 : Str::parseCallback($this->handler, '__invoke');
1457
1458 $policyHandler .= '@' . $method;
1459 }
1460
1461 return $policyHandler ?: [$this, 'defaultPolicyHandler'];
1462 }
1463
1464 /**
1465 * Check if the policy handler is parseable.
1466 *
1467 * @param string $policyHandler
1468 * @return boolean
1469 */
1470 protected function isPolicyHandlerParseable($policyHandler)
1471 {
1472 return (strpos($policyHandler, '@') === true
1473 || strpos($policyHandler, '::') === true);
1474 }
1475
1476 /**
1477 * Default/Fallback policy handler for the route
1478 *
1479 * @return bool
1480 */
1481 public function defaultPolicyHandler()
1482 {
1483 return true;
1484 }
1485
1486 /**
1487 * Parse the rest and permission/policy handlers
1488 *
1489 * @param \WP_REST_Request $request
1490 * @return null
1491 * @throws \BadMethodCallException
1492 */
1493 public function prepareCallbacks($request)
1494 {
1495 $handler = $this->app->parseRestHandler($this->handler, $this->namespace);
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 {
1547 if ($handler instanceof Closure) {
1548 return ['Closure', null];
1549 }
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 {
1582 try {
1583 $policyHandler = $this->app->parsePolicyHandler(
1584 $this->getPolicyHandler($this->policyHandler)
1585 );
1586
1587 if ($policyHandler) {
1588 $this->permissionHandler = $policyHandler;
1589
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];
1595 }
1596 }
1597
1598 if (!is_callable($this->permissionHandler)) {
1599 throw new Exception;
1600 }
1601 }
1602
1603 } catch (Exception $e) {
1604 throw $this->invalidPolicyHandlerException();
1605 }
1606
1607 // Convert object controller to class string for endpoint metadata
1608 if (is_array($policyHandler) && is_object($policyHandler[0])) {
1609 $policyHandler[0] = get_class($policyHandler[0]);
1610 }
1611
1612 return $policyHandler;
1613 }
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;
1623
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];
1628 }
1629
1630 return new BadMethodCallException(
1631 "The permission callback {$pHandler} is invalid or not callable."
1632 );
1633 }
1634
1635 /**
1636 * Get one or more route parameters
1637 * @param string $key
1638 *
1639 * @return mixed
1640 */
1641 public function getParameter($key = null)
1642 {
1643 if (is_null($this->parameters)) {
1644 $this->parameters = $this->app->request->get_url_params();
1645 }
1646
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();
1678 }
1679
1680 /**
1681 * Dynamically access a route parameter.
1682 *
1683 * @param string $key
1684 * @return mixed
1685 */
1686 public function __get($key)
1687 {
1688 return $this->getParameter($key);
1689 }
1690 }
1691