From 45372bf44b8d53947af72da3dbece39d0cdb3076 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 19:19:16 +0100 Subject: [PATCH 01/20] Add class `ResponseHeaders` --- formwork/src/Http/Client.php | 4 +- formwork/src/Http/FileResponse.php | 10 ++-- formwork/src/Http/Header.php | 16 ++++++ formwork/src/Http/HeadersData.php | 4 +- formwork/src/Http/Response.php | 11 ++-- formwork/src/Http/ResponseHeaders.php | 76 +++++++++++++++++++++++++ formwork/src/Http/ResponseInterface.php | 4 +- formwork/src/Updater/Updater.php | 2 +- 8 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 formwork/src/Http/ResponseHeaders.php diff --git a/formwork/src/Http/Client.php b/formwork/src/Http/Client.php index 2fdb15c24..336a4f758 100644 --- a/formwork/src/Http/Client.php +++ b/formwork/src/Http/Client.php @@ -91,10 +91,8 @@ public function fetch(string $uri, array $options = []): Response /** * @param array $options - * - * @return array */ - public function fetchHeaders(string $uri, array $options = []): array + public function fetchHeaders(string $uri, array $options = []): ResponseHeaders { $options += [ 'method' => 'HEAD', diff --git a/formwork/src/Http/FileResponse.php b/formwork/src/Http/FileResponse.php index fdf8cb296..8275bd1a0 100644 --- a/formwork/src/Http/FileResponse.php +++ b/formwork/src/Http/FileResponse.php @@ -88,8 +88,8 @@ public function prepare(Request $request): static { parent::prepare($request); - if (!isset($this->headers['Accept-Ranges']) && in_array($request->method(), [RequestMethod::HEAD, RequestMethod::GET], true)) { - $this->headers['Accept-Ranges'] = 'bytes'; + if (!$this->headers->has('Accept-Ranges') && in_array($request->method(), [RequestMethod::HEAD, RequestMethod::GET], true)) { + $this->headers->set('Accept-Ranges', 'bytes'); } if ($request->method() === RequestMethod::HEAD || $this->requiresEmptyContent()) { @@ -112,12 +112,12 @@ public function prepare(Request $request): static if ($start > $end) { $this->length = 0; $this->responseStatus = ResponseStatus::RangeNotSatisfiable; - $this->headers['Content-Range'] = sprintf('bytes */%s', $this->fileSize); + $this->headers->set('Content-Range', sprintf('bytes */%s', $this->fileSize)); } else { $this->length = (int) ($end - $start + 1); $this->responseStatus = ResponseStatus::PartialContent; - $this->headers['Content-Range'] = sprintf('bytes %s-%s/%s', $start, $end, $this->fileSize); - $this->headers['Content-Length'] = sprintf('%s', $this->length); + $this->headers->set('Content-Range', sprintf('bytes %s-%s/%s', $start, $end, $this->fileSize)); + $this->headers->set('Content-Length', sprintf('%s', $this->length)); } } diff --git a/formwork/src/Http/Header.php b/formwork/src/Http/Header.php index 85ecd521e..e7e059f5a 100644 --- a/formwork/src/Http/Header.php +++ b/formwork/src/Http/Header.php @@ -3,6 +3,7 @@ namespace Formwork\Http; use Formwork\Traits\StaticClass; +use Formwork\Utils\Arr; use RuntimeException; use UnexpectedValueException; @@ -68,4 +69,19 @@ public static function parseQualityValues(string $header): array arsort($result); return $result; } + + public static function fixHeaderName(string $name): string + { + return str_replace('_', '-', ucwords(strtolower($name), '_-')); + } + + /** + * @param array $headers + * + * @return array + */ + public static function fixHeaderNames(array $headers): array + { + return Arr::mapKeys($headers, fn (string $key) => static::fixHeaderName($key)); + } } diff --git a/formwork/src/Http/HeadersData.php b/formwork/src/Http/HeadersData.php index 3b1bef016..32f2875e1 100644 --- a/formwork/src/Http/HeadersData.php +++ b/formwork/src/Http/HeadersData.php @@ -2,8 +2,6 @@ namespace Formwork\Http; -use Formwork\Utils\Arr; - class HeadersData extends RequestData { /** @@ -19,7 +17,7 @@ public function __construct(array $data) */ protected function initialize(array $headers): void { - $this->data = Arr::mapKeys($headers, fn (string $key) => str_replace('_', '-', ucwords(strtolower($key), '_'))); + $this->data = Header::fixHeaderNames($headers); ksort($this->data); } } diff --git a/formwork/src/Http/Response.php b/formwork/src/Http/Response.php index 0979e681d..a94fb6827 100644 --- a/formwork/src/Http/Response.php +++ b/formwork/src/Http/Response.php @@ -2,16 +2,15 @@ namespace Formwork\Http; +use Formwork\Http\Header as HttpHeader; use Formwork\Http\Utils\Header; class Response implements ResponseInterface { /** * Response HTTP headers - * - * @var array */ - protected array $headers; + protected ResponseHeaders $headers; /** * Create a new Response instance @@ -25,7 +24,7 @@ public function __construct(protected string $content, protected ResponseStatus 'Content-Length' => (string) strlen($content), 'Content-Type' => Header::make(['text/html', 'charset' => 'utf-8']), ]; - $this->headers = $headers; + $this->headers = new ResponseHeaders($headers); } public static function __set_state(array $properties): static @@ -52,7 +51,7 @@ public function status(): ResponseStatus /** * Return HTTP headers */ - public function headers(): array + public function headers(): ResponseHeaders { return $this->headers; } @@ -103,7 +102,7 @@ public function toArray(): array return [ 'content' => $this->content, 'status' => $this->responseStatus, - 'headers' => $this->headers, + 'headers' => $this->headers->toArray(), ]; } diff --git a/formwork/src/Http/ResponseHeaders.php b/formwork/src/Http/ResponseHeaders.php new file mode 100644 index 000000000..d6d866480 --- /dev/null +++ b/formwork/src/Http/ResponseHeaders.php @@ -0,0 +1,76 @@ + + */ +class ResponseHeaders implements Arrayable, Countable, Iterator +{ + use DataArrayable; + use DataCountableIterator; + use DataMultipleGetter { + has as protected baseHas; + get as protected baseGet; + } + use DataMultipleSetter { + set as protected baseSet; + remove as protected baseRemove; + } + + /** + * Create a new instance + * + * @param array $data + */ + public function __construct(array $data) + { + $this->initialize($data); + } + + /** + * Return whether data is present + */ + public function isEmpty(): bool + { + return count($this) === 0; + } + + public function has(string $key): bool + { + return $this->baseHas(Header::fixHeaderName($key)); + } + + public function get(string $key, mixed $default = null): mixed + { + return $this->baseGet(Header::fixHeaderName($key), $default); + } + + public function set(string $key, mixed $value): void + { + $this->baseSet(Header::fixHeaderName($key), $value); + ksort($this->data); + } + + public function remove(string $key): void + { + $this->baseRemove(Header::fixHeaderName($key)); + } + + /** + * @param array $headers + */ + protected function initialize(array $headers): void + { + $this->data = Header::fixHeaderNames($headers); + ksort($this->data); + } +} diff --git a/formwork/src/Http/ResponseInterface.php b/formwork/src/Http/ResponseInterface.php index 99cf2dc3e..f3ecc5449 100644 --- a/formwork/src/Http/ResponseInterface.php +++ b/formwork/src/Http/ResponseInterface.php @@ -30,10 +30,8 @@ public function status(): ResponseStatus; /** * Return HTTP headers - * - * @return array */ - public function headers(): array; + public function headers(): ResponseHeaders; /** * Prepare response according to the given HTTP request diff --git a/formwork/src/Updater/Updater.php b/formwork/src/Updater/Updater.php index 04fa5257c..f4676d436 100644 --- a/formwork/src/Updater/Updater.php +++ b/formwork/src/Updater/Updater.php @@ -263,7 +263,7 @@ protected function loadRelease(): void */ protected function getHeaders(): array { - return $this->headers ?? ($this->headers = $this->client->fetchHeaders($this->release['archive'])); + return $this->headers ?? ($this->headers = $this->client->fetchHeaders($this->release['archive'])->toArray()); } /** From 5fd1bde7bf6d24ad2d2f00f60085478fa386e288 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 19:20:56 +0100 Subject: [PATCH 02/20] Collect pending headers --- formwork/src/Http/Response.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/formwork/src/Http/Response.php b/formwork/src/Http/Response.php index a94fb6827..5f3d2eb1d 100644 --- a/formwork/src/Http/Response.php +++ b/formwork/src/Http/Response.php @@ -83,6 +83,15 @@ public function sendHeaders(): void { $this->sendStatus(); + foreach (headers_list() as $header) { + [$name, $value] = HttpHeader::split($header, ':'); + if (!$this->headers->has($name)) { + $this->headers->set($name, $value); + } + } + + header_remove(); + foreach ($this->headers as $fieldName => $fieldValue) { Header::send($fieldName, $fieldValue); } From 8f399efaf57264c6d0e2feb4f5a637174b588926 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 19:21:41 +0100 Subject: [PATCH 03/20] HTTP/1.1 by default --- formwork/src/Http/Utils/Header.php | 2 +- formwork/views/errors/install.php | 2 +- formwork/views/errors/phpversion.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/formwork/src/Http/Utils/Header.php b/formwork/src/Http/Utils/Header.php index c6cc6fb3f..55fff1c0e 100644 --- a/formwork/src/Http/Utils/Header.php +++ b/formwork/src/Http/Utils/Header.php @@ -22,7 +22,7 @@ class Header */ public static function status(ResponseStatus $responseStatus, bool $send = true, bool $exit = false) { - $protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0'; + $protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1'; $responseStatus = implode(' ', [$protocol, $responseStatus->value]); if (!$send) { return $responseStatus; diff --git a/formwork/views/errors/install.php b/formwork/views/errors/install.php index 55243eda8..56f76e479 100644 --- a/formwork/views/errors/install.php +++ b/formwork/views/errors/install.php @@ -1,4 +1,4 @@ - +

The site is currently offline
due to technical problems

If you are the maintainer of this site, please run composer install. Composer autoloader was not found.

diff --git a/formwork/views/errors/phpversion.php b/formwork/views/errors/phpversion.php index 30072b434..10a56228b 100644 --- a/formwork/views/errors/phpversion.php +++ b/formwork/views/errors/phpversion.php @@ -1,4 +1,4 @@ - +

The site is currently offline
due to technical problems

If you are the maintainer of this site, please switch to a PHP version supported by the installed release of Formwork.

From 455266a05cdfa5fa9b3e7dc17fb9c521c65bf8b2 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 19:22:38 +0100 Subject: [PATCH 04/20] Add default `Cache-Control` header --- formwork/src/Http/Response.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/formwork/src/Http/Response.php b/formwork/src/Http/Response.php index 5f3d2eb1d..66eab96f2 100644 --- a/formwork/src/Http/Response.php +++ b/formwork/src/Http/Response.php @@ -92,6 +92,10 @@ public function sendHeaders(): void header_remove(); + if (!$this->headers->has('Cache-Control')) { + $this->headers->set('Cache-Control', 'no-cache, private'); + } + foreach ($this->headers as $fieldName => $fieldValue) { Header::send($fieldName, $fieldValue); } From 62ac542d8e2aa70d82ebac8ee3dc97256b9c1dd5 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 19:23:54 +0100 Subject: [PATCH 05/20] Prevent `session_start()` from setting cache headers --- formwork/src/Http/Session/Session.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/formwork/src/Http/Session/Session.php b/formwork/src/Http/Session/Session.php index 648f16692..1ac6cdfc8 100644 --- a/formwork/src/Http/Session/Session.php +++ b/formwork/src/Http/Session/Session.php @@ -83,6 +83,8 @@ public function start(): void } session_start([ + 'cache_limiter' => '', + 'cache_expire' => 0, 'use_strict_mode' => true, ]); From 6f43d30b84a404f7fcc1d7b6e6188619b34d12e6 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 19:24:14 +0100 Subject: [PATCH 06/20] Handle conditional requests --- formwork/src/Http/Response.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/formwork/src/Http/Response.php b/formwork/src/Http/Response.php index 66eab96f2..9a1e23dfa 100644 --- a/formwork/src/Http/Response.php +++ b/formwork/src/Http/Response.php @@ -61,6 +61,14 @@ public function headers(): ResponseHeaders */ public function prepare(Request $request): static { + if ($this->headers->has('ETag') && $request->headers()->get('If-None-Match') === $this->headers->get('ETag')) { + $this->responseStatus = ResponseStatus::NotModified; + } + + if ($this->headers->has('Last-Modidfied') && $request->headers()->get('If-Modified-Since') === $this->headers->get('Last-Modified')) { + $this->responseStatus = ResponseStatus::NotModified; + } + if ($request->method() === RequestMethod::HEAD || $this->requiresEmptyContent()) { $this->content = ''; } From fd60bed3a71cf7b9cc8b34f8a2cfa046db4d539b Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 19:30:25 +0100 Subject: [PATCH 07/20] Add cache headers to assets --- formwork/src/Controllers/AssetController.php | 2 +- formwork/src/Controllers/PageController.php | 3 ++- formwork/src/Panel/Controllers/UsersController.php | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/formwork/src/Controllers/AssetController.php b/formwork/src/Controllers/AssetController.php index 7f6530f29..313009453 100644 --- a/formwork/src/Controllers/AssetController.php +++ b/formwork/src/Controllers/AssetController.php @@ -14,7 +14,7 @@ public function load(RouteParams $routeParams): FileResponse $path = FileSystem::joinPaths($this->config->get('system.images.processPath'), $routeParams->get('id'), $routeParams->get('name')); if (FileSystem::isFile($path)) { - return new FileResponse($path); + return new FileResponse($path, headers: ['Cache-Control' => 'private, max-age=31536000, immutable']); } throw new FileNotFoundException('Cannot find asset'); diff --git a/formwork/src/Controllers/PageController.php b/formwork/src/Controllers/PageController.php index 63d897f41..5b7a98d25 100644 --- a/formwork/src/Controllers/PageController.php +++ b/formwork/src/Controllers/PageController.php @@ -85,7 +85,8 @@ public function load(RouteParams $routeParams, Statistics $statistics): Response } if ((($parent = $this->site->findPage($upperLevel)) !== null) && $parent->files()->has($filename)) { - return new FileResponse($parent->files()->get($filename)->path()); + $file = $parent->files()->get($filename); + return new FileResponse($file->path(), headers: ['ETag' => $file->hash(), 'Last-Modified' => gmdate('D, d M Y H:i:s T', $file->lastModifiedTime())]); } } diff --git a/formwork/src/Panel/Controllers/UsersController.php b/formwork/src/Panel/Controllers/UsersController.php index e672426c5..886b1b86b 100644 --- a/formwork/src/Panel/Controllers/UsersController.php +++ b/formwork/src/Panel/Controllers/UsersController.php @@ -218,7 +218,7 @@ public function images(RouteParams $routeParams): Response $path = FileSystem::joinPaths($this->config->get('system.users.paths.images'), $routeParams->get('image')); if (FileSystem::isFile($path)) { - return new FileResponse($path, headers: ['Cache-Control' => 'max-age=31536000, private']); + return new FileResponse($path, headers: ['Cache-Control' => 'private, max-age=31536000, immutable']); } throw new FileNotFoundException('Cannot find asset'); From 727da74b586ae1b8811592e8d33915e8c37513d3 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 19:31:52 +0100 Subject: [PATCH 08/20] By default make page requests conditional if cache is enabled --- formwork/src/Controllers/PageController.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/formwork/src/Controllers/PageController.php b/formwork/src/Controllers/PageController.php index 5b7a98d25..c150bb75c 100644 --- a/formwork/src/Controllers/PageController.php +++ b/formwork/src/Controllers/PageController.php @@ -110,6 +110,15 @@ protected function getPageResponse(Page $page): Response $cacheKey = $page->uri(includeLanguage: true); + $headers = []; + + if ($config->get('system.cache.enabled') && $page->contentFile() !== null) { + $headers = [ + 'ETag' => $page->contentFile()->hash(), + 'Last-Modified' => gmdate('D, d M Y H:i:s T', $page->contentFile()->lastModifiedTime()), + ]; + } + if ($config->get('system.cache.enabled') && $this->filesCache->has($cacheKey)) { /** * @var int @@ -123,7 +132,7 @@ protected function getPageResponse(Page $page): Response $this->filesCache->delete($cacheKey); } - $response = new Response($page->render(), $page->responseStatus(), $page->headers()); + $response = new Response($page->render(), $page->responseStatus(), $page->headers() + $headers); if ($config->get('system.cache.enabled') && $page->cacheable()) { $this->filesCache->save($cacheKey, $response); From ad6fbb7a2f56f4c8adce88764dbdc0497cb751fb Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Fri, 1 Nov 2024 22:43:26 +0100 Subject: [PATCH 09/20] Avoid altering cookies in `Response::sendHeaders()` --- formwork/src/Http/Response.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/formwork/src/Http/Response.php b/formwork/src/Http/Response.php index 9a1e23dfa..2c5c2af57 100644 --- a/formwork/src/Http/Response.php +++ b/formwork/src/Http/Response.php @@ -93,13 +93,15 @@ public function sendHeaders(): void foreach (headers_list() as $header) { [$name, $value] = HttpHeader::split($header, ':'); + if (strcasecmp($name, 'Set-Cookie') === 0) { + continue; + } if (!$this->headers->has($name)) { $this->headers->set($name, $value); } + header_remove($name); } - header_remove(); - if (!$this->headers->has('Cache-Control')) { $this->headers->set('Cache-Control', 'no-cache, private'); } From 7be86cae0cf5f41313fc8ced20867df085f5ba48 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:15:37 +0100 Subject: [PATCH 10/20] FIx misspelled header --- formwork/src/Http/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/formwork/src/Http/Response.php b/formwork/src/Http/Response.php index 2c5c2af57..939c780ed 100644 --- a/formwork/src/Http/Response.php +++ b/formwork/src/Http/Response.php @@ -65,7 +65,7 @@ public function prepare(Request $request): static $this->responseStatus = ResponseStatus::NotModified; } - if ($this->headers->has('Last-Modidfied') && $request->headers()->get('If-Modified-Since') === $this->headers->get('Last-Modified')) { + if ($this->headers->has('Last-Modified') && $request->headers()->get('If-Modified-Since') === $this->headers->get('Last-Modified')) { $this->responseStatus = ResponseStatus::NotModified; } From ea6357af70226a83a3b2297dc64b02ac081b100c Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:24:50 +0100 Subject: [PATCH 11/20] Add `autoEtag` and `autoLastModified` params to `FileResponse` constructor --- formwork/src/Controllers/PageController.php | 2 +- formwork/src/Http/FileResponse.php | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/formwork/src/Controllers/PageController.php b/formwork/src/Controllers/PageController.php index c150bb75c..b68b40756 100644 --- a/formwork/src/Controllers/PageController.php +++ b/formwork/src/Controllers/PageController.php @@ -86,7 +86,7 @@ public function load(RouteParams $routeParams, Statistics $statistics): Response if ((($parent = $this->site->findPage($upperLevel)) !== null) && $parent->files()->has($filename)) { $file = $parent->files()->get($filename); - return new FileResponse($file->path(), headers: ['ETag' => $file->hash(), 'Last-Modified' => gmdate('D, d M Y H:i:s T', $file->lastModifiedTime())]); + return new FileResponse($file->path(), autoEtag: true, autoLastModified: true); } } diff --git a/formwork/src/Http/FileResponse.php b/formwork/src/Http/FileResponse.php index 8275bd1a0..778e4ed79 100644 --- a/formwork/src/Http/FileResponse.php +++ b/formwork/src/Http/FileResponse.php @@ -19,8 +19,14 @@ class FileResponse extends Response /** * @inheritdoc */ - public function __construct(protected string $path, ResponseStatus $responseStatus = ResponseStatus::OK, array $headers = [], bool $download = false) - { + public function __construct( + protected string $path, + ResponseStatus $responseStatus = ResponseStatus::OK, + array $headers = [], + bool $download = false, + protected bool $autoEtag = false, + protected bool $autoLastModified = false + ) { $this->fileSize = FileSystem::fileSize($path); $headers += [ @@ -86,6 +92,14 @@ public function send(): void public function prepare(Request $request): static { + if ($this->autoEtag && !$this->headers->has('ETag')) { + $this->headers->set('ETag', hash('sha256', $this->path . ':' . FileSystem::lastModifiedTime($this->path))); + } + + if ($this->autoLastModified && !$this->headers->has('Last-Modified')) { + $this->headers->set('Last-Modified', gmdate('D, d M Y H:i:s T', FileSystem::lastModifiedTime($this->path))); + } + parent::prepare($request); if (!$this->headers->has('Accept-Ranges') && in_array($request->method(), [RequestMethod::HEAD, RequestMethod::GET], true)) { From 6d847ae529d069ad0050b7dd831515a49d7b50ad Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:27:09 +0100 Subject: [PATCH 12/20] Add safe plaintext extensions to get proper MIME type --- formwork/src/Utils/MimeType.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/formwork/src/Utils/MimeType.php b/formwork/src/Utils/MimeType.php index 75754327c..ac184edc0 100644 --- a/formwork/src/Utils/MimeType.php +++ b/formwork/src/Utils/MimeType.php @@ -16,6 +16,8 @@ class MimeType */ protected const DEFAULT_MIME_TYPE = 'application/octet-stream'; + protected const SAFE_PLAINTEXT_EXTENSIONS = ['css', 'js', 'htm', 'html', 'md', 'markdown', 'yml', 'yaml', 'json']; + /** * Associative array containing common MIME types * @@ -143,6 +145,10 @@ public static function fromFile(string $file): string $mimeType = finfo_file($finfo, $file); finfo_close($finfo); + if ($mimeType === 'text/plain' && in_array($extension = FileSystem::extension($file), self::SAFE_PLAINTEXT_EXTENSIONS, true)) { + $mimeType = static::fromExtension($extension); + } + // Fix type for SVG images without XML declaration if ($mimeType === 'image/svg') { $mimeType = static::fromExtension('svg'); From fff167157a4ec57cceb8afbec6f47a5fcba09d6e Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sat, 2 Nov 2024 00:33:37 +0100 Subject: [PATCH 13/20] Handle assets requests uniformly --- .htaccess | 4 +-- formwork/routes.php | 6 +++- formwork/server.php | 4 --- formwork/src/Controllers/AssetController.php | 22 ------------ formwork/src/Controllers/AssetsController.php | 36 +++++++++++++++++++ .../Panel/Controllers/AssetsController.php | 25 +++++++++++++ .../src/Panel/Controllers/UsersController.php | 2 +- formwork/src/Panel/Panel.php | 10 +----- panel/routes.php | 12 ++++++- panel/views/partials/sidebar.php | 2 +- 10 files changed, 81 insertions(+), 42 deletions(-) delete mode 100644 formwork/src/Controllers/AssetController.php create mode 100644 formwork/src/Controllers/AssetsController.php create mode 100644 formwork/src/Panel/Controllers/AssetsController.php diff --git a/.htaccess b/.htaccess index bc017f48f..3f1d35a34 100644 --- a/.htaccess +++ b/.htaccess @@ -13,9 +13,7 @@ AddDefaultCharset utf-8 RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^.* index.php [L] - ## Prevent direct access to Formwork folders but allow access to assets - RewriteRule ^site/templates/assets/.* - [L] - RewriteRule ^panel/assets/.* - [L] + ## Prevent direct access to Formwork folders RewriteRule ^(panel|backup|bin|cache|formwork|site|vendor)/.* index.php [L,NC] ## Prevent access to specific files diff --git a/formwork/routes.php b/formwork/routes.php index 739f52faa..477a2d9ab 100644 --- a/formwork/routes.php +++ b/formwork/routes.php @@ -24,7 +24,11 @@ ], 'assets' => [ 'path' => '/assets/{id}/{name}/', - 'action' => 'Formwork\Controllers\AssetController@load', + 'action' => 'Formwork\Controllers\AssetsController@asset', + ], + 'assets.template' => [ + 'path' => '/site/templates/assets/{file}/', + 'action' => 'Formwork\Controllers\AssetsController@template', ], 'tag.pagination' => [ 'path' => '/{page}/tag/{tagName:aln}/page/{paginationPage:num}/', diff --git a/formwork/server.php b/formwork/server.php index 2cc395a94..3c41fac31 100644 --- a/formwork/server.php +++ b/formwork/server.php @@ -6,10 +6,6 @@ // Emulate the `mod_rewrite` rules defined in .htaccess if ($path !== '/index.php' && is_file($root . $path)) { switch (true) { - case preg_match('~^/site/templates/assets/.*~i', $path): - case preg_match('~^/panel/assets/.*~i', $path): - return false; - case preg_match('~^/(panel|backup|bin|cache|formwork|site|vendor)/.*~i', $path): case preg_match('~^/(.*)\.(md|yml|yaml|json|neon)/?$~i', $path): case preg_match('~^/(\.(.*)|LICENSE|composer\.lock)/?$~i', $path): diff --git a/formwork/src/Controllers/AssetController.php b/formwork/src/Controllers/AssetController.php deleted file mode 100644 index 313009453..000000000 --- a/formwork/src/Controllers/AssetController.php +++ /dev/null @@ -1,22 +0,0 @@ -config->get('system.images.processPath'), $routeParams->get('id'), $routeParams->get('name')); - - if (FileSystem::isFile($path)) { - return new FileResponse($path, headers: ['Cache-Control' => 'private, max-age=31536000, immutable']); - } - - throw new FileNotFoundException('Cannot find asset'); - } -} diff --git a/formwork/src/Controllers/AssetsController.php b/formwork/src/Controllers/AssetsController.php new file mode 100644 index 000000000..958f03c26 --- /dev/null +++ b/formwork/src/Controllers/AssetsController.php @@ -0,0 +1,36 @@ +config->get('system.images.processPath'), $routeParams->get('id'), $routeParams->get('name')); + + if (FileSystem::isFile($path)) { + return new FileResponse($path, headers: ['Cache-Control' => 'private, max-age=31536000, immutable'], autoEtag: true, autoLastModified: true); + } + + throw new FileNotFoundException('Cannot find asset'); + } + + public function template(RouteParams $routeParams): FileResponse + { + $path = FileSystem::joinPaths($this->config->get('system.templates.path'), 'assets', $routeParams->get('file')); + + if (FileSystem::isFile($path)) { + $headers = $this->request->query()->has('v') + ? ['Cache-Control' => 'private, max-age=31536000, immutable'] + : []; + return new FileResponse($path, headers: $headers, autoEtag: true, autoLastModified: true); + } + + throw new FileNotFoundException('Cannot find asset'); + } +} diff --git a/formwork/src/Panel/Controllers/AssetsController.php b/formwork/src/Panel/Controllers/AssetsController.php new file mode 100644 index 000000000..32d4f5860 --- /dev/null +++ b/formwork/src/Panel/Controllers/AssetsController.php @@ -0,0 +1,25 @@ +config->get('system.panel.paths.assets'), $routeParams->get('type'), $routeParams->get('file')); + + if (FileSystem::isFile($path)) { + $headers = ($this->request->query()->has('v') || $routeParams->get('type') === 'icons') + ? ['Cache-Control' => 'private, max-age=31536000, immutable'] + : []; + return new FileResponse($path, headers: $headers, autoEtag: true, autoLastModified: true); + } + + throw new FileNotFoundException('Cannot find asset'); + } +} diff --git a/formwork/src/Panel/Controllers/UsersController.php b/formwork/src/Panel/Controllers/UsersController.php index 886b1b86b..dbb536d25 100644 --- a/formwork/src/Panel/Controllers/UsersController.php +++ b/formwork/src/Panel/Controllers/UsersController.php @@ -218,7 +218,7 @@ public function images(RouteParams $routeParams): Response $path = FileSystem::joinPaths($this->config->get('system.users.paths.images'), $routeParams->get('image')); if (FileSystem::isFile($path)) { - return new FileResponse($path, headers: ['Cache-Control' => 'private, max-age=31536000, immutable']); + return new FileResponse($path, headers: ['Cache-Control' => 'private, max-age=31536000, immutable'], autoEtag: true, autoLastModified: true); } throw new FileNotFoundException('Cannot find asset'); diff --git a/formwork/src/Panel/Panel.php b/formwork/src/Panel/Panel.php index e8e0857b3..464ad5b0a 100644 --- a/formwork/src/Panel/Panel.php +++ b/formwork/src/Panel/Panel.php @@ -62,14 +62,6 @@ public function uri(string $route = ''): string return $this->panelUri() . ltrim($route, '/'); } - /** - * Return a URI relative to the real Panel root - */ - public function realUri(string $route): string - { - return $this->request->root() . 'panel/' . ltrim($route, '/'); - } - /** * Return panel root */ @@ -141,7 +133,7 @@ public function notifications(): array */ public function assets(): Assets { - return $this->assets ?? ($this->assets = new Assets($this->config->get('system.panel.paths.assets'), $this->realUri('/assets/'))); + return $this->assets ?? ($this->assets = new Assets($this->config->get('system.panel.paths.assets'), $this->uri('/assets/'))); } public function colorScheme(): ColorScheme diff --git a/panel/routes.php b/panel/routes.php index a4ed988a8..3b02a0a99 100644 --- a/panel/routes.php +++ b/panel/routes.php @@ -13,6 +13,7 @@ use Formwork\Site; use Formwork\Translations\Translations; use Formwork\Utils\FileSystem; +use Formwork\Utils\Str; use Formwork\View\ViewFactory; return [ @@ -239,6 +240,11 @@ 'methods' => ['GET', 'POST'], ], + 'panel.assets' => [ + 'path' => '/assets/{type:aln}/{file}/', + 'action' => 'Formwork\Panel\Controllers\AssetsController@asset', + ], + 'panel.errors.notfound' => [ 'path' => '/{route}/', 'action' => 'Formwork\Panel\Controllers\ErrorsController@notFound', @@ -324,7 +330,11 @@ 'panel.redirectToLogin' => [ 'action' => static function (Request $request, Site $site, Panel $panel) { // Redirect to login if no user is logged - if (!$site->users()->isEmpty() && !$panel->isLoggedIn() && !in_array($panel->route(), ['/login/', '/logout/'], true)) { + if ( + !$site->users()->isEmpty() && !$panel->isLoggedIn() + && !in_array($panel->route(), ['/login/', '/logout/'], true) + && !Str::startsWith($panel->route(), '/assets/') + ) { $request->session()->set(AuthenticationController::SESSION_REDIRECT_KEY, $panel->route()); return new RedirectResponse($panel->uri('/login/')); } diff --git a/panel/views/partials/sidebar.php b/panel/views/partials/sidebar.php index 86ff6bbe3..c3d7f227e 100644 --- a/panel/views/partials/sidebar.php +++ b/panel/views/partials/sidebar.php @@ -1,6 +1,6 @@