Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Permalinks: look up from index if cache empty #6568

Draft
wants to merge 3 commits into
base: v5/develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions config/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Kirby\Panel\Plugins;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
use Kirby\Uuid\Uuids;

return function (App $kirby) {
$api = $kirby->option('api.slug', 'api');
Expand Down Expand Up @@ -135,11 +136,17 @@
'method' => 'ALL',
'env' => 'site',
'action' => function (string $type, string $id) use ($kirby) {
// try to resolve to model, but only from UUID cache;
// this ensures that only existing UUIDs can be queried
// and attackers can't force Kirby to go through the whole
// site index with a non-existing UUID
if ($model = Uuid::for($type . '://' . $id)?->model(true)) {
// try to resolve to model but if the UUID cache exists
// only allow lookup from the cache;
// only if the cache doesn't exist, use the index;
// this ensures that attackers can't force Kirby to go through
// the whole site index with a non-existing UUID
$lazy = Uuids::cache()->isEmpty() === false;

if ($model = Uuid::for($type . '://' . $id)?->model($lazy)) {
/**
* @var \Kirby\Cms\Page|\Kirby\Cms\File $model
*/
return $kirby
->response()
->redirect($model->url());
Expand Down
15 changes: 15 additions & 0 deletions src/Cache/ApcuCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@
return apcu_clear_cache();
}

/**
* Whether the cache has any entry,
* irrespective whether the entries have expired or not
*/
public function isEmpty(): bool
{
if (empty($this->options['prefix']) === false) {
$iterator = new APCUIterator('!^' . preg_quote($this->options['prefix']) . '!');
} else {
$iterator = new APCUIterator();

Check failure on line 57 in src/Cache/ApcuCache.php

View workflow job for this annotation

GitHub Actions / Unit tests - PHP 8.2

TooFewArguments

src/Cache/ApcuCache.php:57:16: TooFewArguments: Too few arguments for APCUIterator::__construct - expecting search to be passed (see https://psalm.dev/025)

Check failure on line 57 in src/Cache/ApcuCache.php

View workflow job for this annotation

GitHub Actions / Unit tests - PHP 8.3

TooFewArguments

src/Cache/ApcuCache.php:57:16: TooFewArguments: Too few arguments for APCUIterator::__construct - expecting search to be passed (see https://psalm.dev/025)

Check failure

Code scanning / Psalm

TooFewArguments Error

Too few arguments for APCUIterator::__construct - expecting search to be passed
}

return $iterator->getTotalCount() === 0;
}

/**
* Removes an item from the cache and returns
* whether the operation was successful
Expand Down
17 changes: 9 additions & 8 deletions src/Cache/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,12 @@
*/
abstract class Cache
{
/**
* Stores all options for the driver
*/
protected array $options = [];

/**
* Sets all parameters which are needed to connect to the cache storage
*/
public function __construct(array $options = [])
{
$this->options = $options;
public function __construct(
protected array $options = []
) {
}

/**
Expand Down Expand Up @@ -182,6 +177,12 @@ public function getOrSet(
return $result;
}

/**
* Whether the cache has any entry,
* irrespective whether the entries have expired or not
*/
abstract public function isEmpty(): bool;

/**
* Adds the prefix to the key if given
*/
Expand Down
114 changes: 62 additions & 52 deletions src/Cache/FileCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ class FileCache extends Cache
* 'prefix' (default: none)
* 'extension' (file extension for cache files, default: none)
*/
public function __construct(array $options)
{
public function __construct(
array $options
) {
parent::__construct([
'root' => null,
'prefix' => null,
Expand All @@ -51,20 +52,28 @@ public function __construct(array $options)
}

/**
* Returns whether the cache is ready to
* store values
* Checks when the cache has been created;
* returns the creation timestamp on success
* and false if the item does not exist
*/
public function enabled(): bool
public function created(string $key): int|false
{
return is_writable($this->root) === true;
// use the modification timestamp
// as indicator when the cache has been created/overwritten
clearstatcache();

// get the file for this cache key
$file = $this->file($key);
return file_exists($file) ? filemtime($file) : false;
}

/**
* Returns the full root including prefix
* Returns whether the cache is ready to
* store values
*/
public function root(): string
public function enabled(): bool
{
return $this->root;
return is_writable($this->root) === true;
}

/**
Expand Down Expand Up @@ -116,47 +125,28 @@ protected function file(string $key): string
}

/**
* Writes an item to the cache for a given number of minutes and
* returns whether the operation was successful
*
* <code>
* // put an item in the cache for 15 minutes
* $cache->set('value', 'my value', 15);
* </code>
*/
public function set(string $key, $value, int $minutes = 0): bool
{
$file = $this->file($key);

return F::write($file, (new Value($value, $minutes))->toJson());
}

/**
* Internal method to retrieve the raw cache value;
* needs to return a Value object or null if not found
* Flushes the entire cache and returns
* whether the operation was successful
*/
public function retrieve(string $key): Value|null
public function flush(): bool
{
$file = $this->file($key);
$value = F::read($file);
if (
Dir::remove($this->root) === true &&
Dir::make($this->root) === true
) {
return true;
}

return $value ? Value::fromJson($value) : null;
return false; // @codeCoverageIgnore
}

/**
* Checks when the cache has been created;
* returns the creation timestamp on success
* and false if the item does not exist
* Whether the cache has any entry,
* irrespective whether the entries have expired or not
*/
public function created(string $key): int|false
public function isEmpty(): bool
{
// use the modification timestamp
// as indicator when the cache has been created/overwritten
clearstatcache();

// get the file for this cache key
$file = $this->file($key);
return file_exists($file) ? filemtime($file) : false;
return Dir::isEmpty($this->root);
}

/**
Expand Down Expand Up @@ -209,18 +199,38 @@ protected function removeEmptyDirectories(string $dir): void
}

/**
* Flushes the entire cache and returns
* whether the operation was successful
* Internal method to retrieve the raw cache value;
* needs to return a Value object or null if not found
*/
public function flush(): bool
public function retrieve(string $key): Value|null
{
if (
Dir::remove($this->root) === true &&
Dir::make($this->root) === true
) {
return true;
}
$file = $this->file($key);
$value = F::read($file);

return false; // @codeCoverageIgnore
return $value ? Value::fromJson($value) : null;
}

/**
* Returns the full root including prefix
*/
public function root(): string
{
return $this->root;
}

/**
* Writes an item to the cache for a given number of minutes and
* returns whether the operation was successful
*
* <code>
* // put an item in the cache for 15 minutes
* $cache->set('value', 'my value', 15);
* </code>
*/
public function set(string $key, $value, int $minutes = 0): bool
{
$file = $this->file($key);

return F::write($file, (new Value($value, $minutes))->toJson());
}
}
58 changes: 34 additions & 24 deletions src/Cache/MemCached.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ class MemCached extends Cache
* 'port' (default: 11211)
* 'prefix' (default: null)
*/
public function __construct(array $options = [])
{
public function __construct(
array $options = []
) {
parent::__construct([
'host' => 'localhost',
'port' => 11211,
Expand All @@ -58,30 +59,22 @@ public function enabled(): bool
}

/**
* Writes an item to the cache for a given number of minutes and
* returns whether the operation was successful
*
* <code>
* // put an item in the cache for 15 minutes
* $cache->set('value', 'my value', 15);
* </code>
* Flushes the entire cache and returns
* whether the operation was successful;
* WARNING: Memcached only supports flushing the whole cache at once!
*/
public function set(string $key, $value, int $minutes = 0): bool
public function flush(): bool
{
$key = $this->key($key);
$value = (new Value($value, $minutes))->toJson();
$expires = $this->expiration($minutes);
return $this->connection->set($key, $value, $expires);
return $this->connection->flush();
}

/**
* Internal method to retrieve the raw cache value;
* needs to return a Value object or null if not found
* Whether the cache has any entry,
* irrespective whether the entries have expired or not
*/
public function retrieve(string $key): Value|null
public function isEmpty(): bool
{
$value = $this->connection->get($this->key($key));
return Value::fromJson($value);
return count($this->connection->getAllKeys()) === 0;
}

/**
Expand All @@ -94,12 +87,29 @@ public function remove(string $key): bool
}

/**
* Flushes the entire cache and returns
* whether the operation was successful;
* WARNING: Memcached only supports flushing the whole cache at once!
* Internal method to retrieve the raw cache value;
* needs to return a Value object or null if not found
*/
public function flush(): bool
public function retrieve(string $key): Value|null
{
return $this->connection->flush();
$value = $this->connection->get($this->key($key));
return Value::fromJson($value);
}

/**
* Writes an item to the cache for a given number of minutes and
* returns whether the operation was successful
*
* <code>
* // put an item in the cache for 15 minutes
* $cache->set('value', 'my value', 15);
* </code>
*/
public function set(string $key, $value, int $minutes = 0): bool
{
$key = $this->key($key);
$value = (new Value($value, $minutes))->toJson();
$expires = $this->expiration($minutes);
return $this->connection->set($key, $value, $expires);
}
}
43 changes: 26 additions & 17 deletions src/Cache/MemoryCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,22 @@ public function enabled(): bool
}

/**
* Writes an item to the cache for a given number of minutes and
* returns whether the operation was successful
*
* <code>
* // put an item in the cache for 15 minutes
* $cache->set('value', 'my value', 15);
* </code>
* Flushes the entire cache and returns
* whether the operation was successful
*/
public function set(string $key, $value, int $minutes = 0): bool
public function flush(): bool
{
$this->store[$key] = new Value($value, $minutes);
$this->store = [];
return true;
}

/**
* Internal method to retrieve the raw cache value;
* needs to return a Value object or null if not found
* Whether the cache has any entry,
* irrespective whether the entries have expired or not
*/
public function retrieve(string $key): Value|null
public function isEmpty(): bool
{
return $this->store[$key] ?? null;
return count($this->store) === 0;
}

/**
Expand All @@ -66,12 +61,26 @@ public function remove(string $key): bool
}

/**
* Flushes the entire cache and returns
* whether the operation was successful
* Internal method to retrieve the raw cache value;
* needs to return a Value object or null if not found
*/
public function flush(): bool
public function retrieve(string $key): Value|null
{
$this->store = [];
return $this->store[$key] ?? null;
}

/**
* Writes an item to the cache for a given number of minutes and
* returns whether the operation was successful
*
* <code>
* // put an item in the cache for 15 minutes
* $cache->set('value', 'my value', 15);
* </code>
*/
public function set(string $key, $value, int $minutes = 0): bool
{
$this->store[$key] = new Value($value, $minutes);
return true;
}
}
Loading
Loading