Upgrade to 4.3.0
This commit is contained in:
@@ -59,7 +59,11 @@ class Model
|
||||
isset($schema['type']) === true &&
|
||||
$this->data instanceof $schema['type'] === false
|
||||
) {
|
||||
throw new Exception(sprintf('Invalid model type "%s" expected: "%s"', get_class($this->data), $schema['type']));
|
||||
$class = match ($this->data) {
|
||||
null => 'null',
|
||||
default => get_class($this->data),
|
||||
};
|
||||
throw new Exception(sprintf('Invalid model type "%s" expected: "%s"', $class, $schema['type']));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
*
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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']));
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 === '' ||
|
||||
|
@@ -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()
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -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 = [
|
||||
|
@@ -112,7 +112,7 @@ abstract class Sql
|
||||
public function columnName(string $table, string $column, bool $enforceQualified = false): string|null
|
||||
{
|
||||
// ensure we have clean $table and $column values without qualified identifiers
|
||||
list($table, $column) = $this->splitIdentifier($table, $column);
|
||||
[$table, $column] = $this->splitIdentifier($table, $column);
|
||||
|
||||
// combine the identifiers again
|
||||
if ($this->database->validateColumn($table, $column) === true) {
|
||||
@@ -624,7 +624,7 @@ abstract class Sql
|
||||
$result = [];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
list($table, $columnPart) = $this->splitIdentifier($table, $column);
|
||||
[$table, $columnPart] = $this->splitIdentifier($table, $column);
|
||||
|
||||
if ($this->validateColumn($table, $columnPart) === true) {
|
||||
$result[] = $this->combineIdentifier($table, $columnPart);
|
||||
|
@@ -580,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) {
|
||||
@@ -756,20 +756,32 @@ class F
|
||||
* Sanitize a file's name (without extension)
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public static function safeBasename(string $string): string
|
||||
{
|
||||
$name = static::name($string);
|
||||
return Str::slug($name, '-', 'a-z0-9@._-');
|
||||
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): string
|
||||
{
|
||||
$extension = static::extension($string);
|
||||
return Str::slug($extension);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -20,7 +20,7 @@ class Query
|
||||
/**
|
||||
* The Query data array
|
||||
*/
|
||||
protected array|null $data;
|
||||
protected array|null $data = null;
|
||||
|
||||
/**
|
||||
* Creates a new Query object.
|
||||
|
@@ -59,13 +59,15 @@ class QrCode
|
||||
* @param int|null $size Image width/height in pixels, defaults to a size per module of 4x4
|
||||
* @param string $color Foreground color in hex format
|
||||
* @param string $back Background color in hex format
|
||||
* @param int $border Border size in number of modules
|
||||
*/
|
||||
public function toDataUri(
|
||||
int|null $size = null,
|
||||
string $color = '#000000',
|
||||
string $back = '#ffffff'
|
||||
string $back = '#ffffff',
|
||||
int $border = 4
|
||||
): string {
|
||||
$image = $this->toImage($size, $color, $back);
|
||||
$image = $this->toImage($size, $color, $back, $border);
|
||||
|
||||
ob_start();
|
||||
imagepng($image);
|
||||
@@ -82,14 +84,16 @@ class QrCode
|
||||
* @param int|null $size Image width/height in pixels, defaults to a size per module of 4x4
|
||||
* @param string $color Foreground color in hex format
|
||||
* @param string $back Background color in hex format
|
||||
* @param int $border Border size in number of modules
|
||||
*/
|
||||
public function toImage(
|
||||
int|null $size = null,
|
||||
string $color = '#000000',
|
||||
string $back = '#ffffff'
|
||||
string $back = '#ffffff',
|
||||
int $border = 4
|
||||
): GdImage {
|
||||
// get code and size measurements
|
||||
$code = $this->encode();
|
||||
$code = $this->encode($border);
|
||||
[$width, $height] = $this->measure($code);
|
||||
$size ??= ceil($width * 4);
|
||||
$ws = $size / $width;
|
||||
@@ -132,13 +136,15 @@ class QrCode
|
||||
* @param int|string|null $size Optional CSS width of the `<svg>` element
|
||||
* @param string $color Foreground color in hex format
|
||||
* @param string $back Background color in hex format
|
||||
* @param int $border Border size in number of modules
|
||||
*/
|
||||
public function toSvg(
|
||||
int|string|null $size = null,
|
||||
string $color = '#000000',
|
||||
string $back = '#ffffff'
|
||||
string $back = '#ffffff',
|
||||
int $border = 4
|
||||
): string {
|
||||
$code = $this->encode();
|
||||
$code = $this->encode($border);
|
||||
[$vbw, $vbh] = $this->measure($code);
|
||||
|
||||
$modules = $this->eachModuleGroup(
|
||||
@@ -167,15 +173,17 @@ class QrCode
|
||||
* @param int|string|null $size Optional image width/height in pixels (defaults to a size per module of 4x4) or CSS width of the `<svg>` element
|
||||
* @param string $color Foreground color in hex format
|
||||
* @param string $back Background color in hex format
|
||||
* @param int $border Border size in number of modules
|
||||
*/
|
||||
public function write(
|
||||
string $file,
|
||||
int|string|null $size = null,
|
||||
string $color = '#000000',
|
||||
string $back = '#ffffff'
|
||||
string $back = '#ffffff',
|
||||
int $border = 4
|
||||
): void {
|
||||
$format = F::extension($file);
|
||||
$args = [$size, $color, $back];
|
||||
$args = [$size, $color, $back, $border];
|
||||
|
||||
match ($format) {
|
||||
'gif' => imagegif($this->toImage(...$args), $file),
|
||||
@@ -395,7 +403,7 @@ class QrCode
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function encode(): array
|
||||
protected function encode(int $q = 4): array
|
||||
{
|
||||
[$data, $version, $ecl, $ec] = $this->encodeData();
|
||||
$data = $this->encodeErrorCorrection($data, $ec, $version);
|
||||
@@ -404,7 +412,7 @@ class QrCode
|
||||
$mtx = $this->finalizeMatrix($mtx, $size, $ecl, $mask, $version);
|
||||
|
||||
return [
|
||||
'q' => [4, 4, 4, 4],
|
||||
'q' => [$q, $q, $q, $q],
|
||||
'size' => [$size, $size],
|
||||
'bits' => $mtx
|
||||
];
|
||||
|
@@ -56,13 +56,11 @@ class Assets
|
||||
*/
|
||||
public function css(): array
|
||||
{
|
||||
$css = A::merge(
|
||||
[
|
||||
'index' => $this->url . '/css/style.min.css',
|
||||
'plugins' => $this->plugins->url('css')
|
||||
],
|
||||
$this->custom('panel.css')
|
||||
);
|
||||
$css = [
|
||||
'index' => $this->url . '/css/style.min.css',
|
||||
'plugins' => $this->plugins->url('css'),
|
||||
...$this->custom('panel.css')
|
||||
];
|
||||
|
||||
// during dev mode we do not need to load
|
||||
// the general stylesheet (as styling will be inlined)
|
||||
@@ -201,8 +199,8 @@ class Assets
|
||||
*/
|
||||
public function icons(): string
|
||||
{
|
||||
$dir = $this->kirby->root('panel') . '/';
|
||||
$dir .= $this->dev ? 'public' : 'dist';
|
||||
$dir = $this->kirby->root('panel') . '/';
|
||||
$dir .= $this->dev ? 'public' : 'dist';
|
||||
$icons = F::read($dir . '/img/icons.svg');
|
||||
$icons = preg_replace('/<!--(.|\s)*?-->/', '', $icons);
|
||||
return $icons;
|
||||
@@ -213,64 +211,55 @@ class Assets
|
||||
*/
|
||||
public function js(): array
|
||||
{
|
||||
$js = A::merge(
|
||||
[
|
||||
'vue' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/js/vue.min.js'
|
||||
],
|
||||
'vendor' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/js/vendor.min.js',
|
||||
'type' => 'module'
|
||||
],
|
||||
'pluginloader' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/js/plugins.js',
|
||||
'type' => 'module'
|
||||
],
|
||||
'plugins' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->plugins->url('js'),
|
||||
'defer' => true
|
||||
]
|
||||
$js = [
|
||||
'vue' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/js/vue.min.js'
|
||||
],
|
||||
A::map($this->custom('panel.js'), fn ($src) => [
|
||||
'vendor' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/js/vendor.min.js',
|
||||
'type' => 'module'
|
||||
],
|
||||
'pluginloader' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/js/plugins.js',
|
||||
'type' => 'module'
|
||||
],
|
||||
'plugins' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->plugins->url('js'),
|
||||
'defer' => true
|
||||
],
|
||||
...A::map($this->custom('panel.js'), fn ($src) => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $src,
|
||||
'type' => 'module'
|
||||
]),
|
||||
[
|
||||
'index' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/js/index.min.js',
|
||||
'type' => 'module'
|
||||
],
|
||||
]
|
||||
);
|
||||
'index' => [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/js/index.min.js',
|
||||
'type' => 'module'
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
// during dev mode, add vite client and adapt
|
||||
// path to `index.js` - vendor does not need
|
||||
// to be loaded in dev mode
|
||||
if ($this->dev === true) {
|
||||
// load the non-minified index.js, remove vendor script and
|
||||
// development version of Vue
|
||||
$js['vendor']['src'] = null;
|
||||
$js['index']['src'] = $this->url . '/src/index.js';
|
||||
$js['vue']['src'] = $this->url . '/node_modules/vue/dist/vue.js';
|
||||
|
||||
// add vite dev client
|
||||
$js['vite'] = [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/@vite/client',
|
||||
'type' => 'module'
|
||||
];
|
||||
|
||||
$js['index'] = [
|
||||
'nonce' => $this->nonce,
|
||||
'src' => $this->url . '/src/index.js',
|
||||
'type' => 'module'
|
||||
];
|
||||
|
||||
// load the development version of Vue
|
||||
$js['vue']['src'] = $this->url . '/node_modules/vue/dist/vue.js';
|
||||
|
||||
// remove the vendor script
|
||||
$js['vendor']['src'] = null;
|
||||
}
|
||||
|
||||
return array_filter($js, fn ($js) => empty($js['src']) === false);
|
||||
|
@@ -219,9 +219,11 @@ class Example
|
||||
$tail = $matches[2][$key];
|
||||
$code = $matches[3][$key];
|
||||
|
||||
$scriptId = trim(preg_replace_callback('!script="(.*?)"!', function ($match) {
|
||||
return trim($match[1]);
|
||||
}, $tail));
|
||||
$scriptId = trim(preg_replace_callback(
|
||||
'!script="(.*?)"!',
|
||||
fn ($match) => trim($match[1]),
|
||||
$tail
|
||||
));
|
||||
|
||||
$scriptBlock = $scripts[$scriptId] ?? null;
|
||||
|
||||
|
@@ -218,7 +218,7 @@ class Sessions
|
||||
* @internal
|
||||
* @param \Kirby\Session\Session $session Session instance to push to the cache
|
||||
*/
|
||||
public function updateCache(Session $session)
|
||||
public function updateCache(Session $session): void
|
||||
{
|
||||
$this->cache[$session->token()] = $session;
|
||||
}
|
||||
|
@@ -177,7 +177,7 @@ class KirbyTag
|
||||
}
|
||||
|
||||
$pos = strpos($tag, ':');
|
||||
$type = trim(substr($tag, 0, $pos ? $pos : null));
|
||||
$type = trim(substr($tag, 0, $pos ?: null));
|
||||
$type = strtolower($type);
|
||||
$attr = static::$types[$type]['attr'] ?? [];
|
||||
|
||||
|
@@ -76,7 +76,7 @@ class Component
|
||||
}
|
||||
|
||||
$this->attrs = $attrs;
|
||||
$this->options = $options = $this->setup($type);
|
||||
$this->options = $options = static::setup($type);
|
||||
$this->methods = $methods = $options['methods'] ?? [];
|
||||
|
||||
foreach ($attrs as $attrName => $attrValue) {
|
||||
|
@@ -464,7 +464,7 @@ class Dom
|
||||
$namespaceUri = null;
|
||||
$itemLocal = $item;
|
||||
if (Str::contains($item, ':') === true) {
|
||||
list($namespaceName, $itemLocal) = explode(':', $item);
|
||||
[$namespaceName, $itemLocal] = explode(':', $item);
|
||||
$namespaceUri = $allowedNamespaces[$namespaceName] ?? null;
|
||||
} else {
|
||||
// list items without namespace are from the default namespace
|
||||
|
@@ -26,7 +26,7 @@ class Escape
|
||||
/**
|
||||
* The internal singleton escaper instance
|
||||
*/
|
||||
protected static Escaper|null $escaper;
|
||||
protected static Escaper|null $escaper = null;
|
||||
|
||||
/**
|
||||
* Escape common HTML attributes data
|
||||
|
@@ -20,7 +20,7 @@ class Html extends Xml
|
||||
/**
|
||||
* An internal store for an HTML entities translation table
|
||||
*/
|
||||
public static array|null $entities;
|
||||
public static array|null $entities = null;
|
||||
|
||||
/**
|
||||
* List of HTML tags that can be used inline
|
||||
|
@@ -386,10 +386,13 @@ class Str
|
||||
$encoded = '';
|
||||
|
||||
for ($i = 0; $i < static::length($string); $i++) {
|
||||
$char = static::substr($string, $i, 1);
|
||||
$char = mb_convert_encoding($char, 'UCS-4BE', 'UTF-8');
|
||||
list(, $code) = unpack('N', $char);
|
||||
$encoded .= rand(1, 2) === 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';';
|
||||
$char = static::substr($string, $i, 1);
|
||||
$char = mb_convert_encoding($char, 'UCS-4BE', 'UTF-8');
|
||||
[, $code] = unpack('N', $char);
|
||||
$encoded .= match (random_int(1, 2)) {
|
||||
1 => '&#' . $code . ';',
|
||||
2 => '&#x' . dechex($code) . ';'
|
||||
};
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
|
@@ -26,7 +26,7 @@ class BlockUuid extends FieldUuid
|
||||
/**
|
||||
* @var \Kirby\Cms\Block|null
|
||||
*/
|
||||
public Identifiable|null $model;
|
||||
public Identifiable|null $model = null;
|
||||
|
||||
/**
|
||||
* Converts content field to a Blocks collection
|
||||
|
@@ -47,7 +47,7 @@ abstract class FieldUuid extends Uuid
|
||||
$parent = Uuid::for($value['parent'])->model();
|
||||
|
||||
if ($field = $parent?->content()->get($value['field'])) {
|
||||
return $this->fieldToCollection($field)->get($value['id']);
|
||||
return static::fieldToCollection($field)->get($value['id']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ class FileUuid extends ModelUuid
|
||||
/**
|
||||
* @var \Kirby\Cms\File|null
|
||||
*/
|
||||
public Identifiable|null $model;
|
||||
public Identifiable|null $model = null;
|
||||
|
||||
/**
|
||||
* Looks up UUID in cache and resolves to file object;
|
||||
|
@@ -20,7 +20,7 @@ abstract class ModelUuid extends Uuid
|
||||
/**
|
||||
* @var \Kirby\Cms\ModelWithContent|null
|
||||
*/
|
||||
public Identifiable|null $model;
|
||||
public Identifiable|null $model = null;
|
||||
|
||||
/**
|
||||
* Looks up UUID in local and global index
|
||||
|
@@ -23,7 +23,7 @@ class PageUuid extends ModelUuid
|
||||
/**
|
||||
* @var \Kirby\Cms\Page|null
|
||||
*/
|
||||
public Identifiable|null $model;
|
||||
public Identifiable|null $model = null;
|
||||
|
||||
/**
|
||||
* Looks up UUID in cache and resolves
|
||||
|
@@ -23,7 +23,7 @@ class SiteUuid extends Uuid
|
||||
/**
|
||||
* @var \Kirby\Cms\Site|null
|
||||
*/
|
||||
public Identifiable|null $model;
|
||||
public Identifiable|null $model = null;
|
||||
|
||||
/*
|
||||
* Returns empty string since
|
||||
|
@@ -26,7 +26,7 @@ class StructureUuid extends FieldUuid
|
||||
/**
|
||||
* @var \Kirby\Cms\StructureObject|null
|
||||
*/
|
||||
public Identifiable|null $model;
|
||||
public Identifiable|null $model = null;
|
||||
|
||||
/**
|
||||
* Converts content field to a Structure collection
|
||||
|
@@ -23,7 +23,7 @@ class UserUuid extends Uuid
|
||||
/**
|
||||
* @var \Kirby\Cms\User|null
|
||||
*/
|
||||
public Identifiable|null $model;
|
||||
public Identifiable|null $model = null;
|
||||
|
||||
/*
|
||||
* Returns the user ID
|
||||
|
Reference in New Issue
Block a user