Skip to content

Commit

Permalink
Merge pull request #607 from getformwork/feature/improved-cache-control
Browse files Browse the repository at this point in the history
Improved cache control
  • Loading branch information
giuscris authored Nov 2, 2024
2 parents 676de50 + 073fc07 commit d1e2e98
Show file tree
Hide file tree
Showing 28 changed files with 312 additions and 79 deletions.
4 changes: 1 addition & 3 deletions .htaccess
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion formwork/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}/',
Expand Down
4 changes: 0 additions & 4 deletions formwork/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 3 additions & 7 deletions formwork/src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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');
Expand Down
22 changes: 0 additions & 22 deletions formwork/src/Controllers/AssetController.php

This file was deleted.

36 changes: 36 additions & 0 deletions formwork/src/Controllers/AssetsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Formwork\Controllers;

use Formwork\Http\FileResponse;
use Formwork\Router\RouteParams;
use Formwork\Utils\Exceptions\FileNotFoundException;
use Formwork\Utils\FileSystem;

class AssetsController extends AbstractController
{
public function asset(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, 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');
}
}
14 changes: 12 additions & 2 deletions formwork/src/Controllers/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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
Expand All @@ -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);
Expand Down
18 changes: 17 additions & 1 deletion formwork/src/Fields/Dynamic/DynamicFieldValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, mixed>
*/
public static Closure $varsLoader;

/**
* @var array<string, mixed>
*/
public static array $vars = [];
protected static array $vars = [];

/**
* Dynamic value computation status
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 1 addition & 3 deletions formwork/src/Http/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,8 @@ public function fetch(string $uri, array $options = []): Response

/**
* @param array<string, mixed> $options
*
* @return array<string, string>
*/
public function fetchHeaders(string $uri, array $options = []): array
public function fetchHeaders(string $uri, array $options = []): ResponseHeaders
{
$options += [
'method' => 'HEAD',
Expand Down
31 changes: 24 additions & 7 deletions formwork/src/Http/FileResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 += [
Expand All @@ -44,6 +50,7 @@ public function send(): void
$length = $this->length ?? $this->fileSize;

if ($length === 0) {
$this->flush();
return;
}

Expand Down Expand Up @@ -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()) {
Expand All @@ -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));
}
}

Expand Down
16 changes: 16 additions & 0 deletions formwork/src/Http/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Formwork\Http;

use Formwork\Traits\StaticClass;
use Formwork\Utils\Arr;
use RuntimeException;
use UnexpectedValueException;

Expand Down Expand Up @@ -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<string, string> $headers
*
* @return array<string, string>
*/
public static function fixHeaderNames(array $headers): array
{
return Arr::mapKeys($headers, fn (string $key) => static::fixHeaderName($key));
}
}
4 changes: 1 addition & 3 deletions formwork/src/Http/HeadersData.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

namespace Formwork\Http;

use Formwork\Utils\Arr;

class HeadersData extends RequestData
{
/**
Expand All @@ -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);
}
}
Loading

0 comments on commit d1e2e98

Please sign in to comment.