← All changes
|
vendor/wpfluent/framework/src/WPFluent/Http/Route.php
+169
-50
1.3.25
→
1.6.5
View file →
| @@ -19,8 +19,10 @@ | ||
| 19 | 19 | use FluentCart\Framework\Http\SubstituteParameters; |
| 20 | 20 | use FluentCart\Framework\Http\Middleware\RateLimiter; |
| 21 | 21 | use FluentCart\Framework\Validator\ValidationException; |
| 22 | 22 | use FluentCart\Framework\Database\Orm\ModelNotFoundException; |
| 23 | +use FluentCart\Framework\Foundation\Exceptions\HttpException; | |
| 24 | +use FluentCart\Framework\Foundation\Exceptions\ExceptionHandler; | |
| 23 | 25 | use FluentCart\Framework\Http\Response\Response as WPFluentResponse; |
| 24 | 26 | |
| 25 | 27 | class Route |
| 26 | 28 | { |
| @@ -44,8 +46,14 @@ | ||
| 44 | 46 | */ |
| 45 | 47 | protected $restNamespace = null; |
| 46 | 48 | |
| 47 | 49 | /** |
| 50 | + * Whether this route should override existing routes at the same URI. | |
| 51 | + * @var bool | |
| 52 | + */ | |
| 53 | + protected $shouldOverride = false; | |
| 54 | + | |
| 55 | + /** | |
| 48 | 56 | * Full URI |
| 49 | 57 | * @var string |
| 50 | 58 | */ |
| 51 | 59 | protected $uri = null; |
| @@ -197,9 +205,8 @@ | ||
| 197 | 205 | |
| 198 | 206 | /** |
| 199 | 207 | * Map the route to be used in front-end. |
| 200 | 208 | * |
| 201 | - * @param mixed $handler | |
| 202 | 209 | * @return self |
| 203 | 210 | */ |
| 204 | 211 | public function preparefrontendHandlers() |
| 205 | 212 | { |
| @@ -230,9 +237,10 @@ | ||
| 230 | 237 | $endpoints = $this->app->endpoints; |
| 231 | 238 | |
| 232 | 239 | $endpoints[$controller]["_{$cb}"] = [ |
| 233 | 240 | 'uri' => $this->uri, |
| 234 | - 'methods' => explode(',', $this->method) | |
| 241 | + 'methods' => explode(',', $this->method), | |
| 242 | + 'policy' => $this->getPolicyName() | |
| 235 | 243 | ]; |
| 236 | 244 | |
| 237 | 245 | // @phpstan-ignore-next-line |
| 238 | 246 | $this->app->endpoints = $endpoints; |
| @@ -240,8 +248,32 @@ | ||
| 240 | 248 | return $this; |
| 241 | 249 | } |
| 242 | 250 | |
| 243 | 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 | + /** | |
| 244 | 276 | * Parse the action from the handler. |
| 245 | 277 | * |
| 246 | 278 | * @param mixed $handler |
| 247 | 279 | * @return string |
| @@ -552,8 +584,12 @@ | ||
| 552 | 584 | * @return void |
| 553 | 585 | */ |
| 554 | 586 | public function injectProp($key, $value) |
| 555 | 587 | { |
| 588 | + if (!$this->endpointSignature) { | |
| 589 | + return; | |
| 590 | + } | |
| 591 | + | |
| 556 | 592 | [$controller, $cbKey] = $this->endpointSignature; |
| 557 | 593 | |
| 558 | 594 | $controllerKey = str_replace('\\', '.', $controller); |
| 559 | 595 | |
| @@ -713,9 +749,9 @@ | ||
| 713 | 749 | return register_rest_route( |
| 714 | 750 | $this->restNamespace, |
| 715 | 751 | $this->getRouteUri(), |
| 716 | 752 | $this->getOptions(), |
| 717 | - $this->override() | |
| 753 | + $this->shouldOverride | |
| 718 | 754 | ); |
| 719 | 755 | } |
| 720 | 756 | |
| 721 | 757 | /** |
| @@ -738,15 +774,17 @@ | ||
| 738 | 774 | return '/' . trim($this->compileRoute($this->uri), '/'); |
| 739 | 775 | } |
| 740 | 776 | |
| 741 | 777 | /** |
| 742 | - * Allow route override if we are testing. | |
| 743 | - * | |
| 744 | - * @return bool | |
| 778 | + * Mark this route to override any existing route at the same URI. | |
| 779 | + * | |
| 780 | + * @return $this | |
| 745 | 781 | */ |
| 746 | - protected function override() | |
| 782 | + public function override() | |
| 747 | 783 | { |
| 748 | - return str_starts_with($this->app->env(), 'testing'); | |
| 784 | + $this->shouldOverride = true; | |
| 785 | + | |
| 786 | + return $this; | |
| 749 | 787 | } |
| 750 | 788 | |
| 751 | 789 | /** |
| 752 | 790 | * Set route options |
| @@ -872,58 +910,80 @@ | ||
| 872 | 910 | } catch (ModelNotFoundException $e) { |
| 873 | 911 | return $this->app->response->sendError([ |
| 874 | 912 | 'message' => $e->getMessage() |
| 875 | 913 | ], 404); |
| 914 | + } catch (HttpException $e) { | |
| 915 | + return $this->renderHttpException($e); | |
| 876 | 916 | } catch (Throwable $e) { |
| 877 | - return $this->handleUnknownException( | |
| 878 | - $e, $this->response ? $this->response->get_headers() : [] | |
| 879 | - ); | |
| 917 | + $headers = $this->response ? $this->response->get_headers() : []; | |
| 918 | + | |
| 919 | + // Consult the plugin's ExceptionHandler registry BEFORE the | |
| 920 | + // production sanitizer. A registered renderable may return | |
| 921 | + // either an HttpException (rendered with full status + safe | |
| 922 | + // message) or a WP_REST_Response (returned verbatim). Null / | |
| 923 | + // no-match falls through to handleUnknownException — the | |
| 924 | + // sanitization default is preserved for any exception not | |
| 925 | + // explicitly opted in. | |
| 926 | + if ($mapped = $this->mapToHandlerResponse($e)) { | |
| 927 | + return $mapped; | |
| 928 | + } | |
| 929 | + | |
| 930 | + return $this->handleUnknownException($e, $headers); | |
| 880 | 931 | } |
| 881 | 932 | } |
| 882 | 933 | |
| 883 | 934 | /** |
| 884 | - * Handle response from route. | |
| 885 | - * | |
| 886 | - * @param \WP_REST_Response $response | |
| 887 | - * @return \WP_REST_Response | |
| 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 | |
| 888 | 950 | */ |
| 889 | - protected function handleResponse($response) | |
| 951 | + protected function mapToHandlerResponse(Throwable $e) | |
| 890 | 952 | { |
| 891 | - if ($response->get_status() >= 400) { | |
| 892 | - $this->fireExceptionEvent( | |
| 893 | - new Exception( | |
| 894 | - $this->extractErrorMessage($response), | |
| 895 | - $response->get_status() | |
| 896 | - ) | |
| 897 | - ); | |
| 953 | + if (!$this->app->bound(ExceptionHandler::class)) { | |
| 954 | + return null; | |
| 898 | 955 | } |
| 899 | 956 | |
| 900 | - return $response; | |
| 901 | - } | |
| 957 | + $handler = $this->app->make(ExceptionHandler::class); | |
| 902 | 958 | |
| 903 | - /** | |
| 904 | - * Extract error message from response data. | |
| 905 | - * | |
| 906 | - * @param \WP_REST_Response $response | |
| 907 | - * @return string | |
| 908 | - */ | |
| 909 | - protected function extractErrorMessage($response) | |
| 910 | - { | |
| 911 | - $data = $response->get_data(); | |
| 959 | + if (!$handler instanceof ExceptionHandler) { | |
| 960 | + return null; | |
| 961 | + } | |
| 912 | 962 | |
| 913 | - if (is_string($data)) { | |
| 914 | - return $data; | |
| 963 | + $result = $handler->render($e, $this->app); | |
| 964 | + | |
| 965 | + if ($result instanceof HttpException) { | |
| 966 | + return $this->renderHttpException($result); | |
| 915 | 967 | } |
| 916 | 968 | |
| 917 | - if (is_array($data) && isset($data['message'])) { | |
| 918 | - return $data['message']; | |
| 969 | + if ($result instanceof WP_REST_Response) { | |
| 970 | + $this->fireExceptionEvent($e); | |
| 971 | + return $result; | |
| 919 | 972 | } |
| 920 | 973 | |
| 921 | - if ($data instanceof WP_Error) { | |
| 922 | - return $data->get_error_message(); | |
| 923 | - } | |
| 974 | + return null; | |
| 975 | + } | |
| 924 | 976 | |
| 925 | - return 'Unknown error'; | |
| 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; | |
| 926 | 986 | } |
| 927 | 987 | |
| 928 | 988 | /** |
| 929 | 989 | * Throw an exception based on the status code. |
| @@ -957,20 +1017,50 @@ | ||
| 957 | 1017 | $data = []; |
| 958 | 1018 | |
| 959 | 1019 | $this->fireExceptionEvent($e); |
| 960 | 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. | |
| 961 | 1025 | if ($this->app->isDebugOn()) { |
| 962 | 1026 | $data = [ |
| 963 | 1027 | 'file' => $e->getFile(), |
| 964 | 1028 | 'line' => $e->getLine(), |
| 965 | 1029 | ]; |
| 1030 | + | |
| 1031 | + $message = $e->getMessage(); | |
| 1032 | + } else { | |
| 1033 | + $message = 'An internal error occurred.'; | |
| 966 | 1034 | } |
| 967 | 1035 | |
| 968 | 1036 | return $this->app->response->sendError([ |
| 969 | 1037 | 'code' => 'plugin_exception', |
| 970 | 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(), | |
| 971 | 1060 | 'message' => $e->getMessage(), |
| 972 | - ], $e->getCode() ?: 500, $headers); | |
| 1061 | + 'data' => $e->getData(), | |
| 1062 | + ], $e->getStatusCode(), $e->getHeaders()); | |
| 973 | 1063 | } |
| 974 | 1064 | |
| 975 | 1065 | /** |
| 976 | 1066 | * Dispatch the route action. |
| @@ -1044,8 +1134,17 @@ | ||
| 1044 | 1134 | * @return void |
| 1045 | 1135 | */ |
| 1046 | 1136 | protected function fireExceptionEvent($exception) |
| 1047 | 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 | + | |
| 1048 | 1147 | if ($this->app->isDebugOn() || defined('FLUENT_BRIDGE_SECRET')) { |
| 1049 | 1148 | $message = sprintf( |
| 1050 | 1149 | "%s in %s:%d\nStack trace:\n%s\n", |
| 1051 | 1150 | $exception->getMessage(), |
| @@ -1052,12 +1151,28 @@ | ||
| 1052 | 1151 | $exception->getFile(), |
| 1053 | 1152 | $exception->getLine(), |
| 1054 | 1153 | $exception->getTraceAsString() |
| 1055 | 1154 | ); |
| 1056 | - | |
| 1155 | + | |
| 1057 | 1156 | error_log($message); |
| 1058 | - | |
| 1157 | + } | |
| 1158 | + | |
| 1159 | + $firing = true; | |
| 1160 | + | |
| 1161 | + try { | |
| 1059 | 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; | |
| 1060 | 1175 | } |
| 1061 | 1176 | } |
| 1062 | 1177 | |
| 1063 | 1178 | /** |
| @@ -1067,8 +1182,10 @@ | ||
| 1067 | 1182 | */ |
| 1068 | 1183 | public function permissionCallback($wpRestRequest) |
| 1069 | 1184 | { |
| 1070 | 1185 | try { |
| 1186 | + $this->parameters = null; | |
| 1187 | + $this->substitutedParameters = null; | |
| 1071 | 1188 | $this->app->instance('route', $this); |
| 1072 | 1189 | $this->app->instance('wprestrequest', $wpRestRequest); |
| 1073 | 1190 | $this->app->request->mergeInputsFromRestRequest($wpRestRequest); |
| 1074 | 1191 | $this->prepareCallbacks($this->app->request); |
| @@ -1103,9 +1220,9 @@ | ||
| 1103 | 1220 | } |
| 1104 | 1221 | |
| 1105 | 1222 | return $response; |
| 1106 | 1223 | |
| 1107 | - } catch (Exception $e) { | |
| 1224 | + } catch (Throwable $e) { | |
| 1108 | 1225 | return new WP_Error( |
| 1109 | 1226 | 'Permission Callback Error', |
| 1110 | 1227 | $e->getMessage(), [ |
| 1111 | 1228 | 'status' => $e->getCode() ?: 403 |
| @@ -1231,9 +1348,11 @@ | ||
| 1231 | 1348 | * @return array |
| 1232 | 1349 | */ |
| 1233 | 1350 | protected function collectMiddleWare($type = 'before') |
| 1234 | 1351 | { |
| 1235 | - $middleware = $this->app['config']->get('middleware', []); | |
| 1352 | + $middleware = $this->app->bound('http.middleware') | |
| 1353 | + ? $this->app['http.middleware'] | |
| 1354 | + : []; | |
| 1236 | 1355 | |
| 1237 | 1356 | $callableMiddleware = Arr::get($middleware, "global.{$type}", []); |
| 1238 | 1357 | |
| 1239 | 1358 | $routeArray = []; |
| @@ -1262,9 +1381,9 @@ | ||
| 1262 | 1381 | if (isset($handler)) { |
| 1263 | 1382 | $this->addMiddlewareInTheStack($callableMiddleware, $handler); |
| 1264 | 1383 | } else { |
| 1265 | 1384 | if (isset($key)) { |
| 1266 | - $mpath = 'config.middleware.route.' . $type; | |
| 1385 | + $mpath = 'app/Http/middleware.php route.' . $type; | |
| 1267 | 1386 | $msg = "No middleware is assigned for the key: {$key} in {$mpath} array."; |
| 1268 | 1387 | } else { |
| 1269 | 1388 | $msg = "Could't resolve middleware."; |
| 1270 | 1389 | } |
| @@ -1429,10 +1548,10 @@ | ||
| 1429 | 1548 | * @return boolean |
| 1430 | 1549 | */ |
| 1431 | 1550 | protected function isPolicyHandlerParseable($policyHandler) |
| 1432 | 1551 | { |
| 1433 | - return (strpos($policyHandler, '@') === true | |
| 1434 | - || strpos($policyHandler, '::') === true); | |
| 1552 | + return (strpos($policyHandler, '@') !== false | |
| 1553 | + || strpos($policyHandler, '::') !== false); | |
| 1435 | 1554 | } |
| 1436 | 1555 | |
| 1437 | 1556 | /** |
| 1438 | 1557 | * Default/Fallback policy handler for the route |