← All changes
|
vendor/wpfluent/framework/src/WPFluent/Http/Route.php
+124
-35
2.6.0
→
2.11.0
View file →
| @@ -19,8 +19,10 @@ | ||
| 19 | 19 | use FluentCommunity\Framework\Http\SubstituteParameters; |
| 20 | 20 | use FluentCommunity\Framework\Http\Middleware\RateLimiter; |
| 21 | 21 | use FluentCommunity\Framework\Validator\ValidationException; |
| 22 | 22 | use FluentCommunity\Framework\Database\Orm\ModelNotFoundException; |
| 23 | +use FluentCommunity\Framework\Foundation\Exceptions\HttpException; | |
| 24 | +use FluentCommunity\Framework\Foundation\Exceptions\ExceptionHandler; | |
| 23 | 25 | use FluentCommunity\Framework\Http\Response\Response as WPFluentResponse; |
| 24 | 26 | |
| 25 | 27 | class Route |
| 26 | 28 | { |
| @@ -203,9 +205,8 @@ | ||
| 203 | 205 | |
| 204 | 206 | /** |
| 205 | 207 | * Map the route to be used in front-end. |
| 206 | 208 | * |
| 207 | - * @param mixed $handler | |
| 208 | 209 | * @return self |
| 209 | 210 | */ |
| 210 | 211 | public function preparefrontendHandlers() |
| 211 | 212 | { |
| @@ -909,52 +910,83 @@ | ||
| 909 | 910 | } catch (ModelNotFoundException $e) { |
| 910 | 911 | return $this->app->response->sendError([ |
| 911 | 912 | 'message' => $e->getMessage() |
| 912 | 913 | ], 404); |
| 914 | + } catch (HttpException $e) { | |
| 915 | + return $this->renderHttpException($e); | |
| 913 | 916 | } catch (Throwable $e) { |
| 914 | - return $this->handleUnknownException( | |
| 915 | - $e, $this->response ? $this->response->get_headers() : [] | |
| 916 | - ); | |
| 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); | |
| 917 | 931 | } |
| 918 | 932 | } |
| 919 | 933 | |
| 920 | 934 | /** |
| 921 | - * Handle response from route. | |
| 922 | - * | |
| 923 | - * @param \WP_REST_Response $response | |
| 924 | - * @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 | |
| 925 | 950 | */ |
| 926 | - protected function handleResponse($response) | |
| 951 | + protected function mapToHandlerResponse(Throwable $e) | |
| 927 | 952 | { |
| 928 | - return $response; | |
| 929 | - } | |
| 953 | + if (!$this->app->bound(ExceptionHandler::class)) { | |
| 954 | + return null; | |
| 955 | + } | |
| 930 | 956 | |
| 931 | - /** | |
| 932 | - * Extract error message from response data. | |
| 933 | - * | |
| 934 | - * @param \WP_REST_Response $response | |
| 935 | - * @return string | |
| 936 | - */ | |
| 937 | - protected function extractErrorMessage($response) | |
| 938 | - { | |
| 939 | - $data = $response->get_data(); | |
| 957 | + $handler = $this->app->make(ExceptionHandler::class); | |
| 940 | 958 | |
| 941 | - if (is_string($data)) { | |
| 942 | - return $data; | |
| 959 | + if (!$handler instanceof ExceptionHandler) { | |
| 960 | + return null; | |
| 943 | 961 | } |
| 944 | 962 | |
| 945 | - if (is_array($data) && isset($data['message'])) { | |
| 946 | - return $data['message']; | |
| 963 | + $result = $handler->render($e, $this->app); | |
| 964 | + | |
| 965 | + if ($result instanceof HttpException) { | |
| 966 | + return $this->renderHttpException($result); | |
| 947 | 967 | } |
| 948 | 968 | |
| 949 | - if ($data instanceof WP_Error) { | |
| 950 | - return $data->get_error_message(); | |
| 969 | + if ($result instanceof WP_REST_Response) { | |
| 970 | + $this->fireExceptionEvent($e); | |
| 971 | + return $result; | |
| 951 | 972 | } |
| 952 | 973 | |
| 953 | - return 'Unknown error'; | |
| 974 | + return null; | |
| 954 | 975 | } |
| 955 | 976 | |
| 956 | 977 | /** |
| 978 | + * Handle response from route. | |
| 979 | + * | |
| 980 | + * @param \WP_REST_Response $response | |
| 981 | + * @return \WP_REST_Response | |
| 982 | + */ | |
| 983 | + protected function handleResponse($response) | |
| 984 | + { | |
| 985 | + return $response; | |
| 986 | + } | |
| 987 | + | |
| 988 | + /** | |
| 957 | 989 | * Throw an exception based on the status code. |
| 958 | 990 | * |
| 959 | 991 | * @param string $message |
| 960 | 992 | * @param int $status |
| @@ -985,20 +1017,50 @@ | ||
| 985 | 1017 | $data = []; |
| 986 | 1018 | |
| 987 | 1019 | $this->fireExceptionEvent($e); |
| 988 | 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. | |
| 989 | 1025 | if ($this->app->isDebugOn()) { |
| 990 | 1026 | $data = [ |
| 991 | 1027 | 'file' => $e->getFile(), |
| 992 | 1028 | 'line' => $e->getLine(), |
| 993 | 1029 | ]; |
| 1030 | + | |
| 1031 | + $message = $e->getMessage(); | |
| 1032 | + } else { | |
| 1033 | + $message = 'An internal error occurred.'; | |
| 994 | 1034 | } |
| 995 | 1035 | |
| 996 | 1036 | return $this->app->response->sendError([ |
| 997 | 1037 | 'code' => 'plugin_exception', |
| 998 | 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(), | |
| 999 | 1060 | 'message' => $e->getMessage(), |
| 1000 | - ], $e->getCode() ?: 500, $headers); | |
| 1061 | + 'data' => $e->getData(), | |
| 1062 | + ], $e->getStatusCode(), $e->getHeaders()); | |
| 1001 | 1063 | } |
| 1002 | 1064 | |
| 1003 | 1065 | /** |
| 1004 | 1066 | * Dispatch the route action. |
| @@ -1072,8 +1134,17 @@ | ||
| 1072 | 1134 | * @return void |
| 1073 | 1135 | */ |
| 1074 | 1136 | protected function fireExceptionEvent($exception) |
| 1075 | 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 | + | |
| 1076 | 1147 | if ($this->app->isDebugOn() || defined('FLUENT_BRIDGE_SECRET')) { |
| 1077 | 1148 | $message = sprintf( |
| 1078 | 1149 | "%s in %s:%d\nStack trace:\n%s\n", |
| 1079 | 1150 | $exception->getMessage(), |
| @@ -1080,12 +1151,28 @@ | ||
| 1080 | 1151 | $exception->getFile(), |
| 1081 | 1152 | $exception->getLine(), |
| 1082 | 1153 | $exception->getTraceAsString() |
| 1083 | 1154 | ); |
| 1084 | - | |
| 1155 | + | |
| 1085 | 1156 | error_log($message); |
| 1086 | - | |
| 1157 | + } | |
| 1158 | + | |
| 1159 | + $firing = true; | |
| 1160 | + | |
| 1161 | + try { | |
| 1087 | 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; | |
| 1088 | 1175 | } |
| 1089 | 1176 | } |
| 1090 | 1177 | |
| 1091 | 1178 | /** |
| @@ -1133,9 +1220,9 @@ | ||
| 1133 | 1220 | } |
| 1134 | 1221 | |
| 1135 | 1222 | return $response; |
| 1136 | 1223 | |
| 1137 | - } catch (Exception $e) { | |
| 1224 | + } catch (Throwable $e) { | |
| 1138 | 1225 | return new WP_Error( |
| 1139 | 1226 | 'Permission Callback Error', |
| 1140 | 1227 | $e->getMessage(), [ |
| 1141 | 1228 | 'status' => $e->getCode() ?: 403 |
| @@ -1261,9 +1348,11 @@ | ||
| 1261 | 1348 | * @return array |
| 1262 | 1349 | */ |
| 1263 | 1350 | protected function collectMiddleWare($type = 'before') |
| 1264 | 1351 | { |
| 1265 | - $middleware = $this->app['config']->get('middleware', []); | |
| 1352 | + $middleware = $this->app->bound('http.middleware') | |
| 1353 | + ? $this->app['http.middleware'] | |
| 1354 | + : []; | |
| 1266 | 1355 | |
| 1267 | 1356 | $callableMiddleware = Arr::get($middleware, "global.{$type}", []); |
| 1268 | 1357 | |
| 1269 | 1358 | $routeArray = []; |
| @@ -1292,9 +1381,9 @@ | ||
| 1292 | 1381 | if (isset($handler)) { |
| 1293 | 1382 | $this->addMiddlewareInTheStack($callableMiddleware, $handler); |
| 1294 | 1383 | } else { |
| 1295 | 1384 | if (isset($key)) { |
| 1296 | - $mpath = 'config.middleware.route.' . $type; | |
| 1385 | + $mpath = 'app/Http/middleware.php route.' . $type; | |
| 1297 | 1386 | $msg = "No middleware is assigned for the key: {$key} in {$mpath} array."; |
| 1298 | 1387 | } else { |
| 1299 | 1388 | $msg = "Could't resolve middleware."; |
| 1300 | 1389 | } |
| @@ -1459,10 +1548,10 @@ | ||
| 1459 | 1548 | * @return boolean |
| 1460 | 1549 | */ |
| 1461 | 1550 | protected function isPolicyHandlerParseable($policyHandler) |
| 1462 | 1551 | { |
| 1463 | - return (strpos($policyHandler, '@') === true | |
| 1464 | - || strpos($policyHandler, '::') === true); | |
| 1552 | + return (strpos($policyHandler, '@') !== false | |
| 1553 | + || strpos($policyHandler, '::') !== false); | |
| 1465 | 1554 | } |
| 1466 | 1555 | |
| 1467 | 1556 | /** |
| 1468 | 1557 | * Default/Fallback policy handler for the route |