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/App.php b/formwork/src/App.php index c859b8cd3..2e3b4d65c 100644 --- a/formwork/src/App.php +++ b/formwork/src/App.php @@ -9,7 +9,6 @@ use Formwork\Config\Config; use Formwork\Controllers\ErrorsController; use Formwork\Controllers\ErrorsControllerInterface; -use Formwork\Fields\Dynamic\DynamicFieldValue; use Formwork\Files\FileFactory; use Formwork\Files\FileUriGenerator; use Formwork\Files\Services\FileUploader; @@ -139,17 +138,15 @@ public function run(): Response try { $this->loadServices($this->container); - $this->loadRoutes(); - - DynamicFieldValue::$vars = $this->container->call(require $this->config()->get('system.fields.dynamic.vars.file')); - $response = $this->router()->dispatch(); } catch (Throwable $throwable) { $controller = $this->container->get(ErrorsControllerInterface::class); $response = $controller->error(throwable: $throwable); } + $this->request()->session()->save(); + $response->prepare($this->request())->send(); return $response; @@ -172,8 +169,7 @@ protected function loadServices(Container $container): void ->alias('request'); $container->define(ErrorsController::class) - ->alias(ErrorsControllerInterface::class) - ->lazy(false); + ->alias(ErrorsControllerInterface::class); $container->define(CsrfToken::class) ->alias('csrfToken'); diff --git a/formwork/src/Controllers/AssetController.php b/formwork/src/Controllers/AssetController.php deleted file mode 100644 index 7f6530f29..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); - } - - 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/Controllers/PageController.php b/formwork/src/Controllers/PageController.php index 63d897f41..b68b40756 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(), autoEtag: true, autoLastModified: true); } } @@ -109,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 @@ -122,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); diff --git a/formwork/src/Fields/Dynamic/DynamicFieldValue.php b/formwork/src/Fields/Dynamic/DynamicFieldValue.php index a0d7e0d39..f25c3e832 100644 --- a/formwork/src/Fields/Dynamic/DynamicFieldValue.php +++ b/formwork/src/Fields/Dynamic/DynamicFieldValue.php @@ -2,16 +2,25 @@ namespace Formwork\Fields\Dynamic; +use Closure; use Formwork\Exceptions\RecursionException; use Formwork\Fields\Field; use Formwork\Interpolator\Interpolator; +use UnexpectedValueException; class DynamicFieldValue { + /** + * Closure used to lazily load vars + * + * @var Closure(): array + */ + public static Closure $varsLoader; + /** * @var array */ - public static array $vars = []; + protected static array $vars = []; /** * Dynamic value computation status @@ -56,6 +65,13 @@ public static function withComputed(string $value, self $dynamic): self */ public function compute(): void { + if (static::$vars === []) { + if (!(static::$varsLoader instanceof Closure)) { + throw new UnexpectedValueException(sprintf('%s() must be set to a valid Closure before computing dynamic field values', __METHOD__)); + } + static::$vars = (static::$varsLoader)(); + } + if ($this->computed) { return; } 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..21e7a5a65 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 += [ @@ -44,6 +50,7 @@ public function send(): void $length = $this->length ?? $this->fileSize; if ($length === 0) { + $this->flush(); return; } @@ -82,14 +89,24 @@ public function send(): void fclose($output); fclose($file); + + $this->flush(); } 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 (!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 +129,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..57a830b99 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; } @@ -62,10 +61,26 @@ public function headers(): array */ 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-Modified') && $request->headers()->get('If-Modified-Since') === $this->headers->get('Last-Modified')) { + $this->responseStatus = ResponseStatus::NotModified; + } + if ($request->method() === RequestMethod::HEAD || $this->requiresEmptyContent()) { $this->content = ''; } + if ($this->requiresEmptyContent()) { + // Disable default MIME type set by PHP + ini_set('default_mimetype', ''); + + $this->headers->remove('Content-Length'); + $this->headers->remove('Content-Type'); + } + return $this; } @@ -84,6 +99,21 @@ public function sendHeaders(): void { $this->sendStatus(); + 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); + } + + if (!$this->headers->has('Cache-Control')) { + $this->headers->set('Cache-Control', 'no-cache, private'); + } + foreach ($this->headers as $fieldName => $fieldValue) { Header::send($fieldName, $fieldValue); } @@ -96,6 +126,7 @@ public function send(): void { $this->sendHeaders(); echo $this->content; + $this->flush(); } public function toArray(): array @@ -103,7 +134,7 @@ public function toArray(): array return [ 'content' => $this->content, 'status' => $this->responseStatus, - 'headers' => $this->headers, + 'headers' => $this->headers->toArray(), ]; } @@ -126,4 +157,13 @@ protected function requiresEmptyContent(): bool { return in_array($this->responseStatus, [ResponseStatus::NoContent, ResponseStatus::NotModified], true); } + + protected function flush(): void + { + if (function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } else { + flush(); + } + } } 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/Http/Session/Session.php b/formwork/src/Http/Session/Session.php index 648f16692..bd0e6f15e 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, ]); @@ -97,6 +99,17 @@ public function start(): void $this->started = true; } + public function save(): void + { + if (!$this->started) { + return; + } + + session_write_close(); + + $this->started = false; + } + public function destroy(): void { session_destroy(); 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/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 e672426c5..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' => 'max-age=31536000, private']); + 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/formwork/src/Services/Loaders/SchemesServiceLoader.php b/formwork/src/Services/Loaders/SchemesServiceLoader.php index 462c0f989..dbde15a88 100644 --- a/formwork/src/Services/Loaders/SchemesServiceLoader.php +++ b/formwork/src/Services/Loaders/SchemesServiceLoader.php @@ -3,6 +3,7 @@ namespace Formwork\Services\Loaders; use Formwork\Config\Config; +use Formwork\Fields\Dynamic\DynamicFieldValue; use Formwork\Fields\FieldFactory; use Formwork\Languages\Languages; use Formwork\Schemes\Schemes; @@ -19,6 +20,8 @@ public function load(Container $container): object { $container->define(FieldFactory::class); + DynamicFieldValue::$varsLoader = fn () => $container->call(require $this->config->get('system.fields.dynamic.vars.file')); + return $container->build(Schemes::class); } @@ -28,5 +31,6 @@ public function load(Container $container): object public function onResolved(object $service, Container $container): void { $service->loadFromPath($this->config->get('system.schemes.paths.system')); + $service->loadFromPath($this->config->get('system.schemes.paths.site')); } } diff --git a/formwork/src/Services/Loaders/SiteServiceLoader.php b/formwork/src/Services/Loaders/SiteServiceLoader.php index ccaacb825..82a5b9d93 100644 --- a/formwork/src/Services/Loaders/SiteServiceLoader.php +++ b/formwork/src/Services/Loaders/SiteServiceLoader.php @@ -17,7 +17,6 @@ public function __construct(protected Config $config, protected Languages $langu public function load(Container $container): Site { - $this->schemes->loadFromPath($this->config->get('system.schemes.paths.site')); $config = $this->config->get('site'); return $container->build(Site::class, ['data' => [ 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()); } /** diff --git a/formwork/src/Utils/MimeType.php b/formwork/src/Utils/MimeType.php index 75754327c..5a44089fd 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,17 @@ public static function fromFile(string $file): string $mimeType = finfo_file($finfo, $file); finfo_close($finfo); + $extension = FileSystem::extension($file); + + if ($mimeType === 'text/plain' && in_array($extension, self::SAFE_PLAINTEXT_EXTENSIONS, true)) { + $mimeType = static::fromExtension($extension); + } + + // Fix type for CSS files with text/x-asm MIME type + if ($mimeType === 'text/x-asm' && $extension === 'css') { + $mimeType = static::fromExtension($extension); + } + // Fix type for SVG images without XML declaration if ($mimeType === 'image/svg') { $mimeType = static::fromExtension('svg'); 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.

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 @@