diff --git a/src/App.php b/src/App.php index eb4e18a876..8cba65b4a3 100644 --- a/src/App.php +++ b/src/App.php @@ -107,11 +107,6 @@ class App private ResponseInterface $response; - /** @var array Extra HTTP headers to send on exit. */ - protected array $responseHeaders = [ - 'cache-control' => 'no-store', // disable caching by default - ]; - /** @var array Modal view that need to be rendered using json output. */ private $portals = []; @@ -170,6 +165,9 @@ public function __construct(array $defaults = []) $this->response = new Response(); } + // disable caching by default + $this->setResponseHeader('cache-control', 'no-store'); + $this->setApp($this); $this->setDefaults($defaults); @@ -389,17 +387,24 @@ public function setResponseStatusCode(int $statusCode): self /** * @return $this */ - public function setResponseHeader(string $name, string $value): self + public function setResponseHeader(string $name, ?string $value): self { - if ($value !== '') { - $this->responseHeaders[$name] = $value; + if ($value === null) { + $this->response = $this->response->withoutHeader($name); } else { - unset($this->responseHeaders[$name]); + $name = preg_replace_callback('~(?response = $this->response->withHeader($name, $value); } return $this; } + /** + * @param array $headers + */ private function setResponseHeaders(array $headers = []): void { foreach ($headers as $name => $value) { @@ -419,23 +424,19 @@ private function setResponseHeaders(array $headers = []): void */ public function terminate($output = '', array $headers = []): void { - if (!isset($headers['content-type'])) { - if (!isset($this->responseHeaders['content-type'])) { - throw new Exception('Content type must be always set'); - } + $this->setResponseHeaders($headers); - $headers['content-type'] = $this->responseHeaders['content-type']; + $type = preg_replace('~;.*~', '', strtolower($this->response->getHeaderLine('content-type'))); // in LC without charset + if ($type === '') { + throw new Exception('Content type must be always set'); } - $type = preg_replace('~;.*~', '', strtolower($headers['content-type'])); // in LC without charset - if ($type === 'application/json') { if (is_string($output)) { $output = $this->decodeJson($output); } $output['portals'] = $this->getRenderedPortals(); - $this->setResponseHeaders($headers); $this->outputResponseJson($output); } elseif (isset($_GET['__atk_tab']) && $type === 'text/html') { // ugly hack for TABS @@ -456,13 +457,11 @@ public function terminate($output = '', array $headers = []): void $output = $this->getTag('script', [], '$(function () {' . $remove_function . $output['atkjs'] . '});') . $output['html']; - $this->setResponseHeaders($headers); - $this->outputResponseHtml($output, $headers); + $this->outputResponseHtml($output); } elseif ($type === 'text/html') { - $this->setResponseHeaders($headers); - $this->outputResponseHtml($output, $headers); + $this->outputResponseHtml($output); } else { - $this->outputResponse($output, $headers); + $this->outputResponse($output); } $this->runCalled = true; // prevent shutdown function from triggering @@ -1110,24 +1109,36 @@ protected function outputResponseUnsafe(string $data): void { http_response_code($this->response->getStatusCode()); + $isCli = \PHP_SAPI === 'cli'; // for phpunit + + /* if (count($headersNew) > 0 && headers_sent() && !$isCli) { + $lateError = new LateOutputError('Headers already sent, more headers cannot be set at this stage'); + if ($this->catchExceptions) { + $this->caughtException($lateError); + $this->outputLateOutputError($lateError); + } + + throw $lateError; + } */ + + if (!headers_sent() || $isCli) { + foreach ($this->response->getHeaders() as $name => $values) { + foreach ($values as $value) { + if (!$isCli) { + header($name . ': ' . $value, false); + } + } + } + } + echo $data; } - /** @var array */ - private static array $_sentHeaders = []; - /** * Output Response to the client. - * - * @param array $headers */ - protected function outputResponse(string $data, array $headers): void + protected function outputResponse(string $data): void { - $headersAll = array_merge($this->responseHeaders, $headers); - unset($headers); - $headersNew = array_diff_assoc($headersAll, self::$_sentHeaders); - unset($headersAll); - foreach (ob_get_status(true) as $status) { if ($status['buffer_used'] !== 0) { $lateError = new LateOutputError('Unexpected output detected'); @@ -1140,34 +1151,6 @@ protected function outputResponse(string $data, array $headers): void } } - $isCli = \PHP_SAPI === 'cli'; // for phpunit - - if (count($headersNew) > 0 && headers_sent() && !$isCli) { - $lateError = new LateOutputError('Headers already sent, more headers cannot be set at this stage'); - if ($this->catchExceptions) { - $this->caughtException($lateError); - $this->outputLateOutputError($lateError); - } - - throw $lateError; - } - - foreach ($headersNew as $k => $v) { - self::$_sentHeaders[$k] = $v; - } - - if (!headers_sent() || $isCli) { - foreach ($headersNew as $k => $v) { - if (!$isCli) { - $kCamelCase = preg_replace_callback('~(?outputResponseUnsafe($data); } @@ -1177,31 +1160,10 @@ protected function outputResponse(string $data, array $headers): void protected function outputLateOutputError(LateOutputError $exception): void { $this->setResponseStatusCode(500); + $this->setResponseHeader('content-type', 'text/plain'); $plainTextMessage = "\n" . '!! FATAL UI ERROR: ' . $exception->getMessage() . ' !!' . "\n"; - $headersAll = ['content-type' => 'text/plain']; - $headersNew = array_diff_assoc($headersAll, self::$_sentHeaders); - unset($headersAll); - - foreach ($headersNew as $k => $v) { - self::$_sentHeaders[$k] = $v; - } - - $isCli = \PHP_SAPI === 'cli'; // for phpunit - - if (!headers_sent() || $isCli) { - foreach ($headersNew as $k => $v) { - if (!$isCli) { - $kCamelCase = preg_replace_callback('~(?outputResponseUnsafe($plainTextMessage); $this->runCalled = true; // prevent shutdown function from triggering @@ -1221,7 +1183,7 @@ private function outputResponseHtml(string $data): void /** * Output JSON response to the client. * - * @param string|array $data + * @param string|array $data */ private function outputResponseJson($data): void { diff --git a/tests/CallbackTest.php b/tests/CallbackTest.php index 70f88dbe35..afdc92079f 100644 --- a/tests/CallbackTest.php +++ b/tests/CallbackTest.php @@ -18,7 +18,7 @@ class AppMock extends App /** * Overriden to allow multiple App::run() calls, prevent sending headers when headers are already sent. */ - protected function outputResponse(string $data, array $headers): void + protected function outputResponse(string $data): void { echo $data; } diff --git a/tests/DemosTest.php b/tests/DemosTest.php index f039afaccb..7f9cac23d4 100644 --- a/tests/DemosTest.php +++ b/tests/DemosTest.php @@ -117,10 +117,6 @@ protected function setSuperglobalsFromRequest(RequestInterface $request): void foreach ($queryArr as $k => $v) { $_POST[$k] = $v; } - - \Closure::bind(function () { - App::$_sentHeaders = []; - }, null, App::class)(); } protected function resetSuperglobals(): void @@ -194,12 +190,10 @@ protected function getClient(): Client $this->resetSuperglobals(); } - $headers = \Closure::bind(fn () => App::$_sentHeaders, null, App::class)(); - // Attach a response to the easy handle with the parsed headers. $response = new Response( $app->getResponse()->getStatusCode(), // @phpstan-ignore-line - $headers, + $app->getResponse()->getHeaders(), // @phpstan-ignore-line class_exists(Utils::class) ? Utils::streamFor($body) : \GuzzleHttp\Psr7\stream_for($body), // @phpstan-ignore-line Utils class present since guzzlehttp/psr7 v1.7 '1.0' );