isEligible()) { return; } $method = Http::method(); $path = Http::path(); if (!$this->shim->shouldEvaluate($method, $path)) { return; } $verdict = $this->shim->evaluate($method, $path, [ 'body' => Http::body(), 'headers' => Http::headers(), 'preview' => Http::previewToken(), ]); if ($verdict === null || $verdict->isProceed()) { return; // fail closed / nothing to do } if ($verdict->isRespond()) { $this->respond($verdict); return; } $this->applyOverrides($verdict); do_action('sarcio_verdict_applied', $verdict, null); } /** * Describe the static response a `respond` verdict carries. Pure, so tests * cover the shape without a process that exits. * * @return array{body:string,headers:array,status:int} */ public static function staticResponse(Verdict $verdict): array { $headers = ['Content-Type' => 'application/json; charset=utf-8']; foreach ($verdict->responseHeaderList() as $header) { if (isset($header['name'], $header['value'])) { $headers[(string) $header['name']] = (string) $header['value']; } } return [ 'body' => (string) wp_json_encode($verdict->responseBody()), 'headers' => $headers, 'status' => $verdict->responseStatus(), ]; } /** * Feed the request the overridden body, publish the patch context, and queue * up the response side. * * A JSON body cannot be rewritten in place — `php://input` is not writable — * so the overridden body is published through `sarcio_context()['body']` as * well as through the superglobals. Handlers that read `$_POST` (which is * nearly all of admin-ajax) need no changes; handlers that decode the raw * body should prefer the context when it is set. */ private function applyOverrides(Verdict $verdict): void { $body = $verdict->body(); if ($body !== null) { $_POST = $body; $_REQUEST = array_merge(is_array($_GET) ? $_GET : [], $body); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the body is the sidecar's signed verdict, not form input; $_GET is only merged, never read } Context::set($verdict, $body); // Headers can still be set here: `init` runs before WordPress emits any // output, on both page loads and admin-ajax. foreach ($verdict->responseHeaders() as $header) { if (isset($header['name'], $header['value']) && !headers_sent()) { header(sprintf('%s: %s', (string) $header['name'], (string) $header['value'])); } } $ops = $verdict->responseTransform(); if ($ops !== []) { self::transformResponse($ops); } } /** * Rewrites the JSON this request answers with. The buffer is ours alone: * it is closed on `shutdown` ahead of WordPress's own flush, together with * any buffer opened after it, so it never outlives the request. * * @param array $ops */ private static function transformResponse(array $ops): void { $level = ob_get_level() + 1; if (!ob_start(static fn (string $buffer): string => Transform::applyToJsonBuffer($buffer, $ops))) { return; } add_action('shutdown', static function () use ($level): void { while (ob_get_level() >= $level) { if (!ob_end_flush()) { return; } } }, 0); } private function respond(Verdict $verdict): void { $response = self::staticResponse($verdict); if (!headers_sent()) { nocache_headers(); foreach ($response['headers'] as $name => $value) { if (strtolower((string) $name) !== 'content-type') { header(sprintf('%s: %s', $name, $value)); } } } wp_send_json($verdict->responseBody(), $response['status'], InlineJson::FLAGS); } }