Upgrade to 4.3.0

This commit is contained in:
Bastian Allgeier
2024-06-13 12:13:00 +02:00
parent 4b55753c46
commit e50c0341fc
87 changed files with 643 additions and 430 deletions

View File

@@ -30,6 +30,7 @@ use Kirby\Toolkit\A;
use Kirby\Toolkit\Config;
use Kirby\Toolkit\Controller;
use Kirby\Toolkit\LazyValue;
use Kirby\Toolkit\Locale;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
use Throwable;
@@ -170,7 +171,7 @@ class App
'roots' => $this->roots(),
'site' => $this->site(),
'urls' => $this->urls(),
'version' => $this->version(),
'version' => static::version(),
];
}
@@ -255,7 +256,7 @@ class App
foreach ($this->options as $key => $value) {
// detect option keys with the `vendor.plugin.option` format
if (preg_match('/^([a-z0-9-]+\.[a-z0-9-]+)\.(.*)$/i', $key, $matches) === 1) {
list(, $plugin, $option) = $matches;
[, $plugin, $option] = $matches;
// verify that it's really a plugin option
if (isset(static::$plugins[str_replace('.', '/', $plugin)]) !== true) {
@@ -538,6 +539,14 @@ class App
return false;
}
/**
* Returns the current language, if set by `static::setCurrentLanguage`
*/
public function currentLanguage(): Language|null
{
return $this->language ??= $this->defaultLanguage();
}
/**
* Returns the default language object
*/
@@ -623,7 +632,7 @@ class App
return Uuid::for($path, $parent?->files())->model();
}
$parent = $parent ?? $this->site();
$parent ??= $this->site();
$id = dirname($path);
$filename = basename($path);
@@ -879,7 +888,8 @@ class App
}
/**
* Returns the current language
* Returns the language by code or shortcut (`default`, `current`).
* Passing `null` is an alias for passing `current`
*/
public function language(string $code = null): Language|null
{
@@ -887,19 +897,11 @@ class App
return null;
}
if ($code === 'default') {
return $this->defaultLanguage();
}
// if requesting a non-default language,
// find it but don't cache it
if ($code !== null) {
return $this->languages()->find($code);
}
// otherwise return language set by `AppTranslation::setCurrentLanguage`
// or default language
return $this->language ??= $this->defaultLanguage();
return match ($code ?? 'current') {
'default' => $this->defaultLanguage(),
'current' => $this->currentLanguage(),
default => $this->languages()->find($code)
};
}
/**
@@ -1141,7 +1143,7 @@ class App
return null;
}
$parent = $parent ?? $this->site();
$parent ??= $this->site();
if ($page = $parent->find($id)) {
/**
@@ -1212,7 +1214,7 @@ class App
* @internal
* @throws \Kirby\Exception\NotFoundException if the home page cannot be found
*/
public function resolve(string $path = null, string $language = null): mixed
public function resolve(string|null $path = null, string|null $language = null): mixed
{
// set the current translation
$this->setCurrentTranslation($language);
@@ -1410,6 +1412,30 @@ class App
);
}
/**
* Load and set the current language if it exists
* Otherwise fall back to the default language
*
* @internal
*/
public function setCurrentLanguage(
string|null $languageCode = null
): Language|null {
if ($this->multilang() === false) {
Locale::set($this->option('locale', 'en_US.utf-8'));
return $this->language = null;
}
$this->language = $this->language($languageCode) ?? $this->defaultLanguage();
Locale::set($this->language->locale());
// add language slug rules to Str class
Str::$language = $this->language->rules();
return $this->language;
}
/**
* Create your own set of languages
*

View File

@@ -713,17 +713,25 @@ trait AppPlugins
*/
public static function plugin(
string $name,
array $extends = null
array $extends = null,
array $info = [],
string|null $root = null,
string|null $version = null
): PLugin|null {
if ($extends === null) {
return static::$plugins[$name] ?? null;
}
// get the correct root for the plugin
$extends['root'] = $extends['root'] ?? dirname(debug_backtrace()[0]['file']);
$plugin = new Plugin(
name: $name,
extends: $extends,
info: $info,
// TODO: Remove fallback to $extends in v7
root: $root ?? $extends['root'] ?? dirname(debug_backtrace()[0]['file']),
version: $version
);
$plugin = new Plugin($name, $extends);
$name = $plugin->name();
$name = $plugin->name();
if (isset(static::$plugins[$name]) === true) {
throw new DuplicateException('The plugin "' . $name . '" has already been registered');
@@ -792,7 +800,11 @@ trait AppPlugins
// register as anonymous plugin (without actual extensions)
// to be picked up by the Panel\Document class when
// rendering the Panel view
static::plugin('plugins/' . $dirname, ['root' => $dir]);
static::plugin(
name: 'plugins/' . $dirname,
extends: [],
root: $dir
);
} else {
continue;
}

View File

@@ -3,7 +3,6 @@
namespace Kirby\Cms;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Locale;
use Kirby\Toolkit\Str;
/**
@@ -104,33 +103,6 @@ trait AppTranslations
return $this->option('panel.language', $defaultCode);
}
/**
* Load and set the current language if it exists
* Otherwise fall back to the default language
*
* @internal
*/
public function setCurrentLanguage(
string $languageCode = null
): Language|null {
if ($this->multilang() === false) {
Locale::set($this->option('locale', 'en_US.utf-8'));
return $this->language = null;
}
$this->language = $this->language($languageCode);
$this->language ??= $this->defaultLanguage();
if ($this->language) {
Locale::set($this->language->locale());
}
// add language slug rules to Str class
Str::$language = $this->language->rules();
return $this->language;
}
/**
* Set the current translation
*
@@ -148,7 +120,7 @@ trait AppTranslations
*/
public function translation(string|null $locale = null): Translation
{
$locale = $locale ?? I18n::locale();
$locale ??= I18n::locale();
$locale = basename($locale);
// prefer loading them from the translations collection

View File

@@ -575,8 +575,8 @@ class Auth
}
// ensure that the category arrays are defined
$log['by-ip'] = $log['by-ip'] ?? [];
$log['by-email'] = $log['by-email'] ?? [];
$log['by-ip'] ??= [];
$log['by-email'] ??= [];
// remove all elements on the top level with different keys (old structure)
$log = array_intersect_key($log, array_flip(['by-ip', 'by-email']));

View File

@@ -65,7 +65,7 @@ class Blueprint
unset($props['model']);
// extend the blueprint in general
$props = $this->extend($props);
$props = static::extend($props);
// apply any blueprint preset
$props = $this->preset($props);
@@ -652,7 +652,7 @@ class Blueprint
}
// extend options if possible
$options = $this->extend($options);
$options = static::extend($options);
foreach ($options as $key => $value) {
$alias = $aliases[$key] ?? null;
@@ -686,7 +686,7 @@ class Blueprint
}
// inject all section extensions
$sectionProps = $this->extend($sectionProps);
$sectionProps = static::extend($sectionProps);
$sections[$sectionName] = $sectionProps = [
...$sectionProps,
@@ -699,14 +699,14 @@ class Blueprint
'name' => $sectionName,
'label' => 'Invalid section type for section "' . $sectionName . '"',
'type' => 'info',
'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types))
'text' => 'The following section types are available: ' . static::helpList(array_keys(Section::$types))
];
} elseif (isset(Section::$types[$type]) === false) {
$sections[$sectionName] = [
'name' => $sectionName,
'label' => 'Invalid section type ("' . $type . '")',
'type' => 'info',
'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types))
'text' => 'The following section types are available: ' . static::helpList(array_keys(Section::$types))
];
}
@@ -764,7 +764,7 @@ class Blueprint
}
// inject all tab extensions
$tabProps = $this->extend($tabProps);
$tabProps = static::extend($tabProps);
// inject a preset if available
$tabProps = $this->preset($tabProps);

View File

@@ -47,7 +47,9 @@ trait FileActions
string|null $extension = null
): static {
if ($sanitize === true) {
$name = F::safeName($name);
// sanitize the basename part only
// as the extension isn't included in $name
$name = F::safeBasename($name, false);
}
// if no extension is passed, make sure to maintain current one
@@ -139,8 +141,8 @@ trait FileActions
// rename and/or resize the file if configured by new blueprint
$create = $file->blueprint()->create();
$file = $file->manipulate($create);
$file = $file->changeExtension($file, $create['format'] ?? null);
$file->manipulate($create);
return $file;
});
@@ -266,7 +268,6 @@ trait FileActions
// we need to already rename it so that the correct file rules
// are applied
$create = $file->blueprint()->create();
$file = $file->changeExtension($file, $create['format'] ?? null);
// run the hook
$arguments = compact('file', 'upload');
@@ -284,6 +285,7 @@ trait FileActions
// resize the file on upload if configured
$file = $file->manipulate($create);
$file = $file->changeExtension($file, $create['format'] ?? null);
// store the content if necessary
// (always create files in the default language)
@@ -384,8 +386,8 @@ trait FileActions
// apply the resizing/crop options from the blueprint
$create = $file->blueprint()->create();
$file = $file->changeExtension($file, $create['format'] ?? null);
$file = $file->manipulate($create);
$file = $file->changeExtension($file, $create['format'] ?? null);
// return a fresh clone
return $file->clone();

View File

@@ -50,7 +50,7 @@ class FileVersion
// content fields
if ($this->original() instanceof File) {
return $this->original()->content()->get($method, $arguments);
return $this->original()->content()->get($method);
}
}

View File

@@ -29,14 +29,20 @@ class Helpers
* ```
*/
public static $deprecations = [
// The internal `$model->contentFile*()` methods have been deprecated
'model-content-file' => true,
// Passing an `info` array inside the `extends` array
// has been deprecated. Pass the individual entries (e.g. root, version)
// directly as named arguments.
// TODO: switch to true in v6
'plugin-extends-root' => false,
// Passing a single space as value to `Xml::attr()` has been
// deprecated. In a future version, passing a single space won't
// render an empty value anymore but a single space.
// To render an empty value, please pass an empty string.
'xml-attr-single-space' => true,
// The internal `$model->contentFile*()` methods have been deprecated
'model-content-file' => true,
];
/**

View File

@@ -42,7 +42,7 @@ class LanguageRules
/**
* Validates if the language can be updated
*/
public static function update(Language $language)
public static function update(Language $language): void
{
static::validLanguageCode($language);
static::validLanguageName($language);

View File

@@ -57,7 +57,12 @@ class Media
}
// try to generate a thumb for the file
return static::thumb($model, $hash, $filename);
try {
return static::thumb($model, $hash, $filename);
} catch (NotFoundException) {
// render the error page if there is no job for this filename
return false;
}
}
/**

View File

@@ -46,7 +46,7 @@ abstract class ModelWithContent implements Identifiable
public static App $kirby;
protected Site|null $site;
protected ContentStorage $storage;
public Collection|null $translations;
public Collection|null $translations = null;
/**
* Store values used to initilaize object

View File

@@ -22,7 +22,7 @@ class PagePicker extends Picker
// remove once our implementation is better
protected Pages|null $items = null;
protected Pages|null $itemsForQuery = null;
protected Page|Site|null $parent;
protected Page|Site|null $parent = null;
/**
* Extends the basic defaults

View File

@@ -26,12 +26,6 @@ use Throwable;
class Plugin
{
protected PluginAssets $assets;
protected array $extends;
protected string $name;
protected string $root;
// caches
protected array|null $info = null;
protected UpdateStatus|null $updateStatus = null;
/**
@@ -40,16 +34,44 @@ class Plugin
*
* @throws \Kirby\Exception\InvalidArgumentException If the plugin name has an invalid format
*/
public function __construct(string $name, array $extends = [])
{
public function __construct(
protected string $name,
protected array $extends = [],
protected array $info = [],
protected string|null $root = null,
protected string|null $version = null,
) {
static::validateName($name);
$this->name = $name;
$this->extends = $extends;
$this->root = $extends['root'] ?? dirname(debug_backtrace()[0]['file']);
$this->info = empty($extends['info']) === false && is_array($extends['info']) ? $extends['info'] : null;
// TODO: Remove in v7
if ($root = $extends['root'] ?? null) {
Helpers::deprecated('Plugin "' . $name . '": Passing the `root` inside the `extends` array has been deprecated. Pass it directly as named argument `root`.', 'plugin-extends-root');
$this->root ??= $root;
unset($this->extends['root']);
}
unset($this->extends['root'], $this->extends['info']);
$this->root ??= dirname(debug_backtrace()[0]['file']);
// TODO: Remove in v7
if ($info = $extends['info'] ?? null) {
Helpers::deprecated('Plugin "' . $name . '": Passing an `info` array inside the `extends` array has been deprecated. Pass the individual entries directly as named `info` argument.', 'plugin-extends-root');
if (empty($info) === false && is_array($info) === true) {
$this->info = [...$info, ...$this->info];
}
unset($this->extends['info']);
}
// read composer.json and use as info fallback
try {
$info = Data::read($this->manifest());
} catch (Exception) {
// there is no manifest file or it is invalid
$info = [];
}
$this->info = [...$info, ...$this->info];
}
/**
@@ -117,22 +139,11 @@ class Plugin
}
/**
* Returns the raw data from composer.json
* Returns the info data (from composer.json)
*/
public function info(): array
{
if (is_array($this->info) === true) {
return $this->info;
}
try {
$info = Data::read($this->manifest());
} catch (Exception) {
// there is no manifest file or it is invalid
$info = [];
}
return $this->info = $info;
return $this->info;
}
/**
@@ -295,17 +306,20 @@ class Plugin
*/
public function version(): string|null
{
$composerName = $this->info()['name'] ?? null;
$version = $this->info()['version'] ?? null;
$name = $this->info()['name'] ?? null;
try {
// if plugin doesn't have version key in composer.json file
// try to get version from "vendor/composer/installed.php"
$version ??= InstalledVersions::getPrettyVersion($composerName);
// try to get version from "vendor/composer/installed.php",
// this is the most reliable source for the version
$version = InstalledVersions::getPrettyVersion($name);
} catch (Throwable) {
return null;
$version = null;
}
// fallback to the version provided in the plugin's index.php: as named
// argument, entry in the info array or from the composer.json file
$version ??= $this->version ?? $this->info()['version'] ?? null;
if (
is_string($version) !== true ||
$version === '' ||

View File

@@ -162,6 +162,24 @@ class System
->toString();
}
/**
* Returns an array with relevant system information
* used for debugging
* @since 4.3.0
*/
public function info(): array
{
return [
'kirby' => $this->app->version(),
'php' => phpversion(),
'server' => $this->serverSoftware(),
'license' => $this->license()->label(),
'languages' => $this->app->languages()->values(
fn ($lang) => $lang->code()
)
];
}
/**
* Create the most important folders
* if they don't exist yet
@@ -379,32 +397,12 @@ class System
return true;
}
/**
* Check for a valid server environment
*/
public function server(): bool
{
return $this->serverSoftware() !== null;
}
/**
* Returns the detected server software
*/
public function serverSoftware(): string|null
public function serverSoftware(): string
{
$servers = $this->app->option('servers', [
'apache',
'caddy',
'litespeed',
'nginx',
'php'
]);
$software = $this->app->environment()->get('SERVER_SOFTWARE', '');
preg_match('!(' . implode('|', A::wrap($servers)) . ')!i', $software, $matches);
return $matches[0] ?? null;
return $this->app->environment()->get('SERVER_SOFTWARE', '');
}
/**
@@ -421,14 +419,13 @@ class System
public function status(): array
{
return [
'accounts' => $this->accounts(),
'content' => $this->content(),
'curl' => $this->curl(),
'sessions' => $this->sessions(),
'mbstring' => $this->mbstring(),
'media' => $this->media(),
'php' => $this->php(),
'server' => $this->server(),
'accounts' => $this->accounts(),
'content' => $this->content(),
'curl' => $this->curl(),
'sessions' => $this->sessions(),
'mbstring' => $this->mbstring(),
'media' => $this->media(),
'php' => $this->php()
];
}

View File

@@ -392,6 +392,46 @@ class UpdateStatus
});
}
/**
* Finds the maximum possible major update
* that is included with the current license
*
* @return string|null Version number of the update or
* `null` if no free update is possible
*/
protected function findMaximumFreeUpdate(): string|null
{
// get the timestamp of included updates
$renewal = $this->app->system()->license()->renewal();
if ($renewal === null || $this->data === null) {
return null;
}
foreach ($this->data['versions'] ?? [] as $entry) {
$initialRelease = $entry['initialRelease'] ?? null;
$latest = $entry['latest'] ?? '';
// skip entries of irrelevant releases
if (
is_string($initialRelease) !== true ||
version_compare($latest, $this->currentVersion, '<=') === true
) {
continue;
}
$timestamp = strtotime($initialRelease);
// update is free if the initial release was before the
// license renewal date
if (is_int($timestamp) === true && $timestamp < $renewal) {
return $latest;
}
}
return null;
}
/**
* Finds the minimum possible security update
* to fix all known vulnerabilities
@@ -655,7 +695,7 @@ class UpdateStatus
];
}
// check if free updates are possible from the current version
// check if updates within the same major version are possible
$latest = $versionEntry['latest'] ?? null;
if (is_string($latest) === true && $latest !== $this->currentVersion) {
return $this->targetData = [
@@ -665,6 +705,19 @@ class UpdateStatus
];
}
// check if the license includes updates to a newer major version
if ($version = $this->findMaximumFreeUpdate()) {
// extract the part before the first dot
// to find the major release page URL
preg_match('/^(\w+)\./', $version, $matches);
return $this->targetData = [
'status' => 'update',
'url' => $this->urlFor($matches[1] . '.0', 'changes'),
'version' => $version
];
}
// no free update is possible, but we are not on the latest version,
// so the overall latest version must be an upgrade
return $this->targetData = [