PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
← 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