Update to Kirby 4.7.0

This commit is contained in:
Paul Nicoué 2025-04-21 18:57:21 +02:00
parent 02a9ab387c
commit ba25a9a198
509 changed files with 26604 additions and 14872 deletions

View file

@ -27,18 +27,19 @@ class Asset
/**
* Relative file path
*/
protected string|null $path = null;
protected string|null $path;
/**
* Creates a new Asset object for the given path.
*/
public function __construct(string $path)
{
$this->setProperties([
'path' => dirname($path),
'root' => $this->kirby()->root('index') . '/' . $path,
'url' => $this->kirby()->url('base') . '/' . $path
]);
$this->root = $this->kirby()->root('index') . '/' . $path;
$this->url = $this->kirby()->url('base') . '/' . $path;
$path = dirname($path);
$this->path = $path === '.' ? '' : $path;
}
/**
@ -46,7 +47,7 @@ class Asset
*
* @throws \Kirby\Exception\BadMethodCallException
*/
public function __call(string $method, array $arguments = [])
public function __call(string $method, array $arguments = []): mixed
{
// public property access
if (isset($this->$method) === true) {
@ -114,15 +115,4 @@ class Asset
{
return $this->path;
}
/**
* Setter for the path
*
* @return $this
*/
protected function setPath(string $path): static
{
$this->path = $path === '.' ? '' : $path;
return $this;
}
}

View file

@ -149,7 +149,7 @@ class Dir
string $dir,
bool $recursive = false,
array|false|null $ignore = [],
string $path = null
string|null $path = null
): array {
$result = [];
$dir = realpath($dir);
@ -169,7 +169,10 @@ class Dir
$result[] = $entry;
if ($recursive === true && is_dir($root) === true) {
$result = array_merge($result, static::index($root, true, $ignore, $entry));
$result = [
...$result,
...static::index($root, true, $ignore, $entry)
];
}
}
@ -217,143 +220,145 @@ class Dir
array|null $contentIgnore = null,
bool $multilang = false
): array {
$dir = realpath($dir);
$inventory = [
'children' => [],
'files' => [],
'template' => 'default',
];
$dir = realpath($dir);
if ($dir === false) {
return $inventory;
}
$items = static::read($dir, $contentIgnore);
// a temporary store for all content files
$content = [];
// sort all items naturally to avoid sorting issues later
// read and sort all items naturally to avoid sorting issues later
$items = static::read($dir, $contentIgnore);
natsort($items);
// loop through all directory items and collect all relevant information
foreach ($items as $item) {
// ignore all items with a leading dot
// ignore all items with a leading dot or underscore
if (in_array(substr($item, 0, 1), ['.', '_']) === true) {
continue;
}
$root = $dir . '/' . $item;
// collect all directories as children
if (is_dir($root) === true) {
// extract the slug and num of the directory
if (preg_match('/^([0-9]+)' . static::$numSeparator . '(.*)$/', $item, $match)) {
$num = (int)$match[1];
$slug = $match[2];
} else {
$num = null;
$slug = $item;
}
$inventory['children'][] = [
'dirname' => $item,
'model' => null,
'num' => $num,
'root' => $root,
'slug' => $slug,
];
} else {
$extension = pathinfo($item, PATHINFO_EXTENSION);
switch ($extension) {
case 'htm':
case 'html':
case 'php':
// don't track those files
break;
case $contentExtension:
$content[] = pathinfo($item, PATHINFO_FILENAME);
break;
default:
$inventory['files'][$item] = [
'filename' => $item,
'extension' => $extension,
'root' => $root,
];
}
}
}
// remove the language codes from all content filenames
if ($multilang === true) {
foreach ($content as $key => $filename) {
$content[$key] = pathinfo($filename, PATHINFO_FILENAME);
$inventory['children'][] = static::inventoryChild(
$item,
$root,
$contentExtension,
$multilang
);
continue;
}
$content = array_unique($content);
$extension = pathinfo($item, PATHINFO_EXTENSION);
// don't track files with these extensions
if (in_array($extension, ['htm', 'html', 'php']) === true) {
continue;
}
// collect all content files separately,
// not as inventory entries
if ($extension === $contentExtension) {
$filename = pathinfo($item, PATHINFO_FILENAME);
// remove the language codes from all content filenames
if ($multilang === true) {
$filename = pathinfo($filename, PATHINFO_FILENAME);
}
$content[] = $filename;
continue;
}
// collect all other files
$inventory['files'][$item] = [
'filename' => $item,
'extension' => $extension,
'root' => $root,
];
}
$inventory = static::inventoryContent($inventory, $content);
$inventory = static::inventoryModels($inventory, $contentExtension, $multilang);
$content = array_unique($content);
$inventory['template'] = static::inventoryTemplate(
$content,
$inventory['files']
);
return $inventory;
}
/**
* Take all content files,
* remove those who are meta files and
* detect the main content file
* Collect information for a child for the inventory
*/
protected static function inventoryContent(array $inventory, array $content): array
{
// filter meta files from the content file
if (empty($content) === true) {
$inventory['template'] = 'default';
return $inventory;
protected static function inventoryChild(
string $item,
string $root,
string $contentExtension = 'txt',
bool $multilang = false
): array {
// extract the slug and num of the directory
if ($separator = strpos($item, static::$numSeparator)) {
$num = (int)substr($item, 0, $separator);
$slug = substr($item, $separator + 1);
}
foreach ($content as $contentName) {
// could be a meta file. i.e. cover.jpg
if (isset($inventory['files'][$contentName]) === true) {
// determine the model
if (empty(Page::$models) === false) {
if ($multilang === true) {
$code = App::instance()->defaultLanguage()->code();
$contentExtension = $code . '.' . $contentExtension;
}
// look if a content file can be found
// for any of the available models
foreach (Page::$models as $modelName => $modelClass) {
if (is_file($root . '/' . $modelName . '.' . $contentExtension) === true) {
$model = $modelName;
break;
}
}
}
return [
'dirname' => $item,
'model' => $model ?? null,
'num' => $num ?? null,
'root' => $root,
'slug' => $slug ?? $item,
];
}
/**
* Determines the main template for the inventory
* from all collected content files, ignore file meta files
*/
protected static function inventoryTemplate(
array $content,
array $files,
): string {
foreach ($content as $name) {
// is a meta file corresponding to an actual file, i.e. cover.jpg
if (isset($files[$name]) === true) {
continue;
}
// it's most likely the template
$inventory['template'] = $contentName;
// (will overwrite and use the last match for historic reasons)
$template = $name;
}
return $inventory;
}
/**
* Go through all inventory children
* and inject a model for each
*/
protected static function inventoryModels(
array $inventory,
string $contentExtension,
bool $multilang = false
): array {
// inject models
if (
empty($inventory['children']) === false &&
empty(Page::$models) === false
) {
if ($multilang === true) {
$contentExtension = App::instance()->defaultLanguage()->code() . '.' . $contentExtension;
}
foreach ($inventory['children'] as $key => $child) {
foreach (Page::$models as $modelName => $modelClass) {
if (file_exists($child['root'] . '/' . $modelName . '.' . $contentExtension) === true) {
$inventory['children'][$key]['model'] = $modelName;
break;
}
}
}
}
return $inventory;
return $template ?? 'default';
}
/**
@ -402,10 +407,8 @@ class Dir
$parent = dirname($dir);
if ($recursive === true) {
if (is_dir($parent) === false) {
static::make($parent, true);
}
if ($recursive === true && is_dir($parent) === false) {
static::make($parent, true);
}
if (is_writable($parent) === false) {
@ -426,19 +429,22 @@ class Dir
* subfolders have been modified for the last time.
*
* @param string $dir The path of the directory
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
* for the globally configured one
*/
public static function modified(string $dir, string $format = null, string $handler = 'date'): int|string
{
public static function modified(
string $dir,
string|null $format = null,
string|null $handler = null
): int|string {
$modified = filemtime($dir);
$items = static::read($dir);
foreach ($items as $item) {
if (is_file($dir . '/' . $item) === true) {
$newModified = filemtime($dir . '/' . $item);
} else {
$newModified = static::modified($dir . '/' . $item);
}
$newModified = match (is_file($dir . '/' . $item)) {
true => filemtime($dir . '/' . $item),
false => static::modified($dir . '/' . $item)
};
$modified = ($newModified > $modified) ? $newModified : $modified;
}
@ -593,7 +599,10 @@ class Dir
return true;
}
if (is_dir($subdir) === true && static::wasModifiedAfter($subdir, $time) === true) {
if (
is_dir($subdir) === true &&
static::wasModifiedAfter($subdir, $time) === true
) {
return true;
}
}

View file

@ -475,12 +475,13 @@ class F
/**
* Get the file's last modification time.
*
* @param string $handler date, intl or strftime
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
* for the globally configured one
*/
public static function modified(
string $file,
string|IntlDateFormatter|null $format = null,
string $handler = 'date'
string|null $handler = null
): string|int|false {
if (file_exists($file) !== true) {
return false;
@ -579,7 +580,7 @@ class F
}
// the math magic
$size = round($size / pow(1024, ($unit = floor(log($size, 1024)))), 2);
$size = round($size / 1024 ** ($unit = floor(log($size, 1024))), 2);
// format the number if requested
if ($locale !== false) {
@ -727,7 +728,8 @@ class F
}
/**
* Sanitize a filename to strip unwanted special characters
* Sanitize a file's full name (filename and extension)
* to strip unwanted special characters
*
* <code>
*
@ -740,12 +742,46 @@ class F
*/
public static function safeName(string $string): string
{
$name = static::name($string);
$extension = static::extension($string);
$safeName = Str::slug($name, '-', 'a-z0-9@._-');
$safeExtension = empty($extension) === false ? '.' . Str::slug($extension) : '';
$basename = static::safeBasename($string);
$extension = static::safeExtension($string);
return $safeName . $safeExtension;
if (empty($extension) === false) {
$extension = '.' . $extension;
}
return $basename . $extension;
}
/**
* Sanitize a file's name (without extension)
* @since 4.0.0
*/
public static function safeBasename(
string $string,
bool $extract = true
): string {
// extract only the name part from whole filename string
if ($extract === true) {
$string = static::name($string);
}
return Str::slug($string, '-', 'a-z0-9@._-');
}
/**
* Sanitize a file's extension
* @since 4.0.0
*/
public static function safeExtension(
string $string,
bool $extract = true
): string {
// extract only the extension part from whole filename string
if ($extract === true) {
$string = static::extension($string);
}
return Str::slug($string);
}
/**
@ -776,11 +812,11 @@ class F
);
}
try {
return filesize($file);
} catch (Throwable) {
return 0;
if ($size = @filesize($file)) {
return $size;
}
return 0;
}
/**

View file

@ -10,7 +10,6 @@ use Kirby\Http\Response;
use Kirby\Sane\Sane;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\Html;
use Kirby\Toolkit\Properties;
use Kirby\Toolkit\V;
/**
@ -27,23 +26,21 @@ use Kirby\Toolkit\V;
*/
class File
{
use Properties;
/**
* Parent file model
* The model object must use the `\Kirby\Filesystem\IsFile` trait
*/
protected object|null $model = null;
protected object|null $model;
/**
* Absolute file path
*/
protected string|null $root = null;
protected string|null $root;
/**
* Absolute file URL
*/
protected string|null $url = null;
protected string|null $url;
/**
* Validation rules to be used for `::match()`
@ -58,6 +55,8 @@ class File
*
* @param array|string|null $props Properties or deprecated `$root` string
* @param string|null $url Deprecated argument, use `$props['url']` instead
*
* @throws \Kirby\Exception\InvalidArgumentException When the model does not use the `Kirby\Filesystem\IsFile` trait
*/
public function __construct(
array|string|null $props = null,
@ -65,7 +64,6 @@ class File
) {
// Legacy support for old constructor of
// the `Kirby\Image\Image` class
// @todo 4.0.0 remove
if (is_array($props) === false) {
$props = [
'root' => $props,
@ -73,11 +71,21 @@ class File
];
}
$this->setProperties($props);
$this->root = $props['root'] ?? null;
$this->url = $props['url'] ?? null;
$this->model = $props['model'] ?? null;
if (
$this->model !== null &&
method_exists($this->model, 'hasIsFileTrait') !== true
) {
throw new InvalidArgumentException('The model object must use the "Kirby\Filesystem\IsFile" trait');
}
}
/**
* Improved `var_dump` output
* @codeCoverageIgnore
*/
public function __debugInfo(): array
{
@ -269,6 +277,15 @@ class File
if (is_array($rules['mime'] ?? null) === true) {
$mime = $this->mime();
// the MIME type could not be determined, but matching
// to it was requested explicitly
if ($mime === null) {
throw new Exception([
'key' => 'file.mime.missing',
'data' => ['filename' => $this->filename()]
]);
}
// determine if any pattern matches the MIME type;
// once any pattern matches, `$carry` is `true` and the rest is skipped
$matches = array_reduce(
@ -343,19 +360,14 @@ class File
/**
* Returns the file's last modification time
*
* @param string|null $handler date, intl or strftime
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
* for the globally configured one
*/
public function modified(
string|IntlDateFormatter|null $format = null,
string|null $handler = null
): string|int|false {
$kirby = $this->kirby();
return F::modified(
$this->root(),
$format,
$handler ?? $kirby?->option('date.handler', 'date') ?? 'date'
);
return F::modified($this->root(), $format, $handler);
}
/**
@ -435,45 +447,6 @@ class File
return $this->root ??= $this->model?->root();
}
/**
* Setter for the parent file model, which uses this instance as proxied file asset
*
* @return $this
*
* @throws \Kirby\Exception\InvalidArgumentException When the model does not use the `Kirby\Filesystem\IsFile` trait
*/
protected function setModel(object|null $model = null): static
{
if ($model !== null && method_exists($model, 'hasIsFileTrait') !== true) {
throw new InvalidArgumentException('The model object must use the "Kirby\Filesystem\IsFile" trait');
}
$this->model = $model;
return $this;
}
/**
* Setter for the root
*
* @return $this
*/
protected function setRoot(string|null $root = null): static
{
$this->root = $root;
return $this;
}
/**
* Setter for the file url
*
* @return $this
*/
protected function setUrl(string|null $url = null): static
{
$this->url = $url;
return $this;
}
/**
* Returns the absolute url for the file
*/

View file

@ -2,6 +2,7 @@
namespace Kirby\Filesystem;
use Kirby\Cms\Language;
use Kirby\Toolkit\Str;
/**
@ -28,44 +29,33 @@ use Kirby\Toolkit\Str;
*/
class Filename
{
/**
* List of all applicable attributes
*/
protected array $attributes;
/**
* The sanitized file extension
*/
protected string $extension;
/**
* The source original filename
*/
protected string $filename;
/**
* The sanitized file name
*/
protected string $name;
/**
* The template for the final name
*/
protected string $template;
/**
* Creates a new Filename object
*
* @param string $template for the final name
* @param array $attributes List of all applicable attributes
*/
public function __construct(string $filename, string $template, array $attributes = [])
{
$this->filename = $filename;
$this->template = $template;
$this->attributes = $attributes;
$this->extension = $this->sanitizeExtension(
public function __construct(
protected string $filename,
protected string $template,
protected array $attributes = [],
protected string|null $language = null
) {
$this->name = $this->sanitizeName($filename);
$this->extension = $this->sanitizeExtension(
$attributes['format'] ??
pathinfo($filename, PATHINFO_EXTENSION)
);
$this->name = $this->sanitizeName(pathinfo($filename, PATHINFO_FILENAME));
}
/**
@ -89,6 +79,7 @@ class Filename
'blur' => $this->blur(),
'bw' => $this->grayscale(),
'q' => $this->quality(),
'sharpen' => $this->sharpen(),
];
$array = array_filter(
@ -227,24 +218,49 @@ class Filename
/**
* Sanitizes the file extension.
* The extension will be converted
* to lowercase and `jpeg` will be
* replaced with `jpg`
* It also replaces `jpeg` with `jpg`.
*/
protected function sanitizeExtension(string $extension): string
{
$extension = strtolower($extension);
$extension = F::safeExtension('test.' . $extension);
$extension = str_replace('jpeg', 'jpg', $extension);
return $extension;
}
/**
* Sanitizes the name with Kirby's
* Str::slug function
* Sanitizes the file name
*/
protected function sanitizeName(string $name): string
{
return Str::slug($name);
// temporarily store language rules
$rules = Str::$language;
// add rules for a particular language to `Str` class
if ($this->language !== null) {
Str::$language = [
...Str::$language,
...Language::loadRules($this->language)];
}
// sanitize name
$name = F::safeBasename($this->filename);
// restore language rules
Str::$language = $rules;
return $name;
}
/**
* Normalizes the sharpen option value
*/
public function sharpen(): int|false
{
return match ($this->attributes['sharpen'] ?? false) {
false => false,
true => 50,
default => (int)$this->attributes['sharpen']
};
}
/**

View file

@ -5,7 +5,6 @@ namespace Kirby\Filesystem;
use Kirby\Cms\App;
use Kirby\Exception\BadMethodCallException;
use Kirby\Image\Image;
use Kirby\Toolkit\Properties;
/**
* Trait for all objects that represent an asset file.
@ -22,8 +21,6 @@ use Kirby\Toolkit\Properties;
*/
trait IsFile
{
use Properties;
/**
* File asset object
*/
@ -32,19 +29,20 @@ trait IsFile
/**
* Absolute file path
*/
protected string|null $root = null;
protected string|null $root;
/**
* Absolute file URL
*/
protected string|null $url = null;
protected string|null $url;
/**
* Constructor sets all file properties
*/
public function __construct(array $props)
{
$this->setProperties($props);
$this->root = $props['root'] ?? null;
$this->url = $props['url'] ?? null;
}
/**
@ -52,7 +50,7 @@ trait IsFile
*
* @throws \Kirby\Exception\BadMethodCallException
*/
public function __call(string $method, array $arguments = [])
public function __call(string $method, array $arguments = []): mixed
{
// public property access
if (isset($this->$method) === true) {
@ -136,28 +134,6 @@ trait IsFile
return $this->root;
}
/**
* Setter for the root
*
* @return $this
*/
protected function setRoot(string|null $root = null): static
{
$this->root = $root;
return $this;
}
/**
* Setter for the file url
*
* @return $this
*/
protected function setUrl(string|null $url = null): static
{
$this->url = $url;
return $this;
}
/**
* Returns the file type
*/

View file

@ -2,6 +2,7 @@
namespace Kirby\Filesystem;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
use SimpleXMLElement;
@ -30,7 +31,7 @@ class Mime
'aifc' => 'audio/x-aiff',
'aiff' => 'audio/x-aiff',
'avi' => 'video/x-msvideo',
'avif' => 'image/avif',
'avif' => 'image/avif',
'bmp' => 'image/bmp',
'css' => 'text/css',
'csv' => ['text/csv', 'text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'],
@ -119,24 +120,22 @@ class Mime
/**
* Fixes an invalid MIME type guess for the given file
*
* @param string $file
* @param string $mime
* @param string $extension
* @return string|null
*/
public static function fix(string $file, string $mime = null, string $extension = null)
{
public static function fix(
string $file,
string|null $mime = null,
string|null $extension = null
): string|null {
// fixing map
$map = [
'text/html' => [
'svg' => ['Kirby\Filesystem\Mime', 'fromSvg'],
'svg' => [Mime::class, 'fromSvg'],
],
'text/plain' => [
'css' => 'text/css',
'json' => 'application/json',
'mjs' => 'text/javascript',
'svg' => ['Kirby\Filesystem\Mime', 'fromSvg'],
'svg' => [Mime::class, 'fromSvg'],
],
'text/x-asm' => [
'css' => 'text/css'
@ -167,9 +166,6 @@ class Mime
/**
* Guesses a MIME type by extension
*
* @param string $extension
* @return string|null
*/
public static function fromExtension(string $extension): string|null
{
@ -179,11 +175,8 @@ class Mime
/**
* Returns the MIME type of a file
*
* @param string $file
* @return string|false
*/
public static function fromFileInfo(string $file)
public static function fromFileInfo(string $file): string|false
{
if (function_exists('finfo_file') === true && file_exists($file) === true) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
@ -197,13 +190,13 @@ class Mime
/**
* Returns the MIME type of a file
*
* @param string $file
* @return string|false
*/
public static function fromMimeContentType(string $file)
public static function fromMimeContentType(string $file): string|false
{
if (function_exists('mime_content_type') === true && file_exists($file) === true) {
if (
function_exists('mime_content_type') === true &&
file_exists($file) === true
) {
return mime_content_type($file);
}
@ -212,11 +205,8 @@ class Mime
/**
* Tries to detect a valid SVG and returns the MIME type accordingly
*
* @param string $file
* @return string|false
*/
public static function fromSvg(string $file)
public static function fromSvg(string $file): string|false
{
if (file_exists($file) === true) {
libxml_use_internal_errors(true);
@ -234,10 +224,6 @@ class Mime
/**
* Tests if a given MIME type is matched by an `Accept` header
* pattern; returns true if the MIME type is contained at all
*
* @param string $mime
* @param string $pattern
* @return bool
*/
public static function isAccepted(string $mime, string $pattern): bool
{
@ -256,10 +242,6 @@ class Mime
* Tests if a MIME wildcard pattern from an `Accept` header
* matches a given type
* @since 3.3.0
*
* @param string $test
* @param string $wildcard
* @return bool
*/
public static function matches(string $test, string $wildcard): bool
{
@ -268,11 +250,8 @@ class Mime
/**
* Returns the extension for a given MIME type
*
* @param string|null $mime
* @return string|false
*/
public static function toExtension(string $mime = null)
public static function toExtension(string|null $mime = null): string|false
{
foreach (static::$types as $key => $value) {
if (is_array($value) === true && in_array($mime, $value) === true) {
@ -289,22 +268,33 @@ class Mime
/**
* Returns all available extensions for a given MIME type
*
* @param string|null $mime
* @return array
*/
public static function toExtensions(string $mime = null): array
public static function toExtensions(string|null $mime = null, bool $matchWildcards = false): array
{
$extensions = [];
$testMime = fn (string $v) => static::matches($v, $mime);
foreach (static::$types as $key => $value) {
if (is_array($value) === true && in_array($mime, $value) === true) {
$extensions[] = $key;
continue;
}
if ($value === $mime) {
$extensions[] = $key;
if (is_array($value) === true) {
if ($matchWildcards === true) {
if (A::some($value, $testMime)) {
$extensions[] = $key;
}
} else {
if (in_array($mime, $value) === true) {
$extensions[] = $key;
}
}
} else {
if ($matchWildcards === true) {
if ($testMime($value) === true) {
$extensions[] = $key;
}
} else {
if ($value === $mime) {
$extensions[] = $key;
}
}
}
}
@ -314,8 +304,10 @@ class Mime
/**
* Returns the MIME type of a file
*/
public static function type(string $file, string|null $extension = null): string|null
{
public static function type(
string $file,
string|null $extension = null
): string|null {
// use the standard finfo extension
$mime = static::fromFileInfo($file);
@ -338,8 +330,6 @@ class Mime
/**
* Returns all detectable MIME types
*
* @return array
*/
public static function types(): array
{