Upgrade to 3.9.0

This commit is contained in:
Bastian Allgeier
2023-01-17 14:50:16 +01:00
parent 0ebe0c7b16
commit 6e5c9d1f48
132 changed files with 1664 additions and 1254 deletions

View File

@@ -23,7 +23,7 @@ class Collection
protected Api $api;
protected $data;
protected $model;
protected $select;
protected $select = null;
protected $view;
/**
@@ -36,7 +36,6 @@ class Collection
$this->api = $api;
$this->data = $data;
$this->model = $schema['model'] ?? null;
$this->select = null;
$this->view = $schema['view'] ?? null;
if ($data === null) {

View File

@@ -91,12 +91,8 @@ class Model
*/
public function selection(): array
{
$select = $this->select;
if ($select === null) {
$select = array_keys($this->fields);
}
$select = $this->select;
$select ??= array_keys($this->fields);
$selection = [];
foreach ($select as $key => $value) {

View File

@@ -16,7 +16,7 @@ use TypeError;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage 3.10
* @codeCoverageIgnore
*/
class Collection extends BaseCollection
@@ -45,21 +45,22 @@ class Collection extends BaseCollection
/**
* Validate the type of every item that is being
* added to the collection. They cneed to have
* added to the collection. They need to have
* the class defined by static::TYPE.
*/
public function __set(string $key, $value): void
{
if (
is_a($value, static::TYPE) === false
) {
if (is_a($value, static::TYPE) === false) {
throw new TypeError('Each value in the collection must be an instance of ' . static::TYPE);
}
parent::__set($key, $value);
}
public static function factory(array $items)
/**
* Creates a collection from a nested array structure
*/
public static function factory(array $items): static
{
$collection = new static();
$className = static::TYPE;
@@ -77,7 +78,11 @@ class Collection extends BaseCollection
return $collection;
}
public function render(ModelWithContent $model)
/**
* Renders each item with a model and returns
* an array of all rendered results
*/
public function render(ModelWithContent $model): array
{
$props = [];

View File

@@ -17,7 +17,7 @@ use Kirby\Filesystem\F;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
class Config

View File

@@ -11,7 +11,7 @@ namespace Kirby\Blueprint;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
class Extension

View File

@@ -16,7 +16,7 @@ use ReflectionUnionType;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
class Factory
@@ -95,7 +95,7 @@ class Factory
}
// union types
if (is_a($propType, ReflectionUnionType::class) === true) {
if ($propType instanceof ReflectionUnionType) {
return static::forUnionType($propType, $value);
}

View File

@@ -13,7 +13,7 @@ use Kirby\Cms\ModelWithContent;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
class Node
@@ -53,7 +53,6 @@ class Node
return Factory::make(static::class, $props);
}
public static function load(string|array $props): static
{
// load by path

View File

@@ -14,7 +14,7 @@ use Kirby\Toolkit\I18n;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
class NodeI18n extends NodeProperty

View File

@@ -11,7 +11,7 @@ namespace Kirby\Blueprint;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
class NodeIcon extends NodeString

View File

@@ -13,7 +13,7 @@ use Kirby\Cms\ModelWithContent;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
abstract class NodeProperty

View File

@@ -13,7 +13,7 @@ use Kirby\Cms\ModelWithContent;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
class NodeString extends NodeProperty

View File

@@ -14,7 +14,7 @@ use Kirby\Cms\ModelWithContent;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* // TODO: include in test coverage in 3.9
* // TODO: include in test coverage in 3.10
* @codeCoverageIgnore
*/
class NodeText extends NodeI18n

View File

@@ -39,7 +39,8 @@ class Value
*
* @param int $minutes the number of minutes until the value expires
* or an absolute UNIX timestamp
* @param int $created the UNIX timestamp when the value has been created
* @param int|null $created the UNIX timestamp when the value has been created
* (defaults to the current time)
*/
public function __construct($value, int $minutes = 0, int|null $created = null)
{

View File

@@ -18,6 +18,7 @@ use Kirby\Http\Router;
use Kirby\Http\Uri;
use Kirby\Http\Visitor;
use Kirby\Session\AutoSession;
use Kirby\Template\Snippet;
use Kirby\Text\KirbyTag;
use Kirby\Text\KirbyTags;
use Kirby\Toolkit\A;
@@ -1278,6 +1279,11 @@ class App
// set the current locale
$this->setCurrentLanguage($language);
// directly prevent path with incomplete content representation
if (Str::endsWith($path, '.') === true) {
return null;
}
// the site is needed a couple times here
$site = $this->site();
@@ -1557,24 +1563,6 @@ class App
return $this;
}
/**
* Returns the Environment object
* @deprecated 3.7.0 Use `$kirby->environment()` instead
*
* @return \Kirby\Http\Environment
* @deprecated Will be removed in Kirby 3.9.0
* @todo Remove in 3.9.0
* @codeCoverageIgnore
*/
public function server()
{
// @codeCoverageIgnoreStart
Helpers::deprecated('$kirby->server() has been deprecated and will be removed in Kirby 3.9.0. Use $kirby->environment() instead.');
// @codeCoverageIgnoreEnd
return $this->environment();
}
/**
* Initializes and returns the Site object
*
@@ -1630,15 +1618,20 @@ class App
* @return string|null
* @psalm-return ($return is true ? string : null)
*/
public function snippet($name, $data = [], bool $return = true): string|null
public function snippet($name, $data = [], bool $return = true, bool $slots = false): Snippet|string|null
{
if (is_object($data) === true) {
$data = ['item' => $data];
}
$snippet = ($this->component('snippet'))($this, $name, array_merge($this->data, $data));
$snippet = ($this->component('snippet'))(
$this,
$name,
array_merge($this->data, $data),
$slots
);
if ($return === true) {
if ($return === true || $slots === true) {
return $snippet;
}
@@ -1661,7 +1654,7 @@ class App
* and return the Template object
*
* @internal
* @return \Kirby\Cms\Template
* @return \Kirby\Template\Template
* @param string $name
* @param string $type
* @param string $defaultType

View File

@@ -82,9 +82,10 @@ trait AppCaches
];
}
$prefix = str_replace(['/', ':'], '_', $this->system()->indexUrl()) .
'/' .
str_replace('.', '/', $key);
$prefix =
str_replace(['/', ':'], '_', $this->system()->indexUrl()) .
'/' .
str_replace('.', '/', $key);
$defaults = [
'active' => true,

View File

@@ -60,10 +60,7 @@ trait AppUsers
}
try {
// TODO: switch over in 3.9.0 to
// return $callback($userAfter);
$proxy = new AppUsersImpersonateProxy($this);
return $callback->call($proxy, $userAfter);
return $callback($userAfter);
} catch (Throwable $e) {
throw $e;
} finally {

View File

@@ -1,32 +0,0 @@
<?php
namespace Kirby\Cms;
/**
* Temporary proxy class to ease transition
* of binding the callback for `$kirby->impersonate()`
*
* @package Kirby Cms
* @author Nico Hoffmann <nico@getkirby.com>,
* Lukas Bestle <lukas@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*
* @internal
* @deprecated Will be removed in Kirby 3.9.0
* @todo remove in 3.9.0
*/
class AppUsersImpersonateProxy
{
public function __construct(protected App $app)
{
}
public function __call($name, $arguments)
{
Helpers::deprecated('Calling $kirby->' . $name . '() as $this->' . $name . '() has been deprecated inside the $kirby->impersonate() callback function. Use a dedicated $kirby object for your call instead of $this. In Kirby 3.9.0 $this will no longer refer to the $kirby object, but the current context of the callback function.');
return $this->app->$name(...$arguments);
}
}

View File

@@ -15,6 +15,7 @@ use Kirby\Http\Idn;
use Kirby\Http\Request\Auth\BasicAuth;
use Kirby\Session\Session;
use Kirby\Toolkit\A;
use SensitiveParameter;
use Throwable;
/**
@@ -381,17 +382,16 @@ class Auth
/**
* Login a user by email and password
*
* @param string $email
* @param string $password
* @param bool $long
* @return \Kirby\Cms\User
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/
public function login(string $email, string $password, bool $long = false)
{
public function login(
string $email,
#[SensitiveParameter]
string $password,
bool $long = false
): User {
// session options
$options = [
'createMode' => 'cookie',
@@ -412,17 +412,16 @@ class Auth
* Login a user by email, password and auth challenge
* @since 3.5.0
*
* @param string $email
* @param string $password
* @param bool $long
* @return \Kirby\Cms\Auth\Status
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/
public function login2fa(string $email, string $password, bool $long = false)
{
public function login2fa(
string $email,
#[SensitiveParameter]
string $password,
bool $long = false
): Status {
$this->validatePassword($email, $password);
return $this->createChallenge($email, $long, '2fa');
}
@@ -516,16 +515,15 @@ class Auth
* Validates the user credentials and returns the user object on success;
* otherwise logs the failed attempt
*
* @param string $email
* @param string $password
* @return \Kirby\Cms\User
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/
public function validatePassword(string $email, string $password)
{
public function validatePassword(
string $email,
#[SensitiveParameter]
string $password
): User {
$email = Idn::decodeEmail($email);
try {
@@ -798,8 +796,10 @@ class Auth
* @throws \Kirby\Exception\InvalidArgumentException If no authentication challenge is active
* @throws \Kirby\Exception\LogicException If the authentication challenge is invalid
*/
public function verifyChallenge(string $code)
{
public function verifyChallenge(
#[SensitiveParameter]
string $code
) {
try {
$session = $this->kirby->session();

View File

@@ -3,6 +3,7 @@
namespace Kirby\Cms\Auth;
use Kirby\Cms\User;
use SensitiveParameter;
/**
* Template class for authentication challenges
@@ -48,8 +49,11 @@ abstract class Challenge
* @param string $code Code to verify
* @return bool
*/
public static function verify(User $user, string $code): bool
{
public static function verify(
User $user,
#[SensitiveParameter]
string $code
): bool {
$hash = $user->kirby()->session()->get('kirby.challenge.code');
if (is_string($hash) !== true) {
return false;

View File

@@ -115,8 +115,9 @@ class ContentTranslation
*/
public function exists(): bool
{
return empty($this->content) === false ||
file_exists($this->contentFile()) === true;
return
empty($this->content) === false ||
file_exists($this->contentFile()) === true;
}
/**

View File

@@ -132,7 +132,7 @@ class Email
*
* @param string $name Template name
* @param string|null $type `html` or `text`
* @return \Kirby\Cms\Template
* @return \Kirby\Template\Template
*/
protected function getTemplate(string $name, string $type = null)
{

View File

@@ -169,11 +169,12 @@ trait FileActions
* way of generating files.
*
* @param array $props
* @param bool $move If set to `true`, the source will be deleted
* @return static
* @throws \Kirby\Exception\InvalidArgumentException
* @throws \Kirby\Exception\LogicException
*/
public static function create(array $props)
public static function create(array $props, bool $move = false)
{
if (isset($props['source'], $props['parent']) === false) {
throw new InvalidArgumentException('Please provide the "source" and "parent" props for the File');
@@ -204,12 +205,16 @@ trait FileActions
$file = $file->clone(['content' => $form->strings(true)]);
// run the hook
return $file->commit('create', compact('file', 'upload'), function ($file, $upload) {
$arguments = compact('file', 'upload');
return $file->commit('create', $arguments, function ($file, $upload) use ($move) {
// remove all public versions, lock and clear UUID cache
$file->unpublish();
// only move the original source if intended
$method = $move === true ? 'move' : 'copy';
// overwrite the original
if (F::copy($upload->root(), $file->root(), true) !== true) {
if (F::$method($upload->root(), $file->root(), true) !== true) {
throw new LogicException('The file could not be created');
}
@@ -280,10 +285,11 @@ trait FileActions
* source.
*
* @param string $source
* @param bool $move If set to `true`, the source will be deleted
* @return static
* @throws \Kirby\Exception\LogicException
*/
public function replace(string $source)
public function replace(string $source, bool $move = false)
{
$file = $this->clone();
@@ -292,12 +298,15 @@ trait FileActions
'upload' => $file->asset($source)
];
return $this->commit('replace', $arguments, function ($file, $upload) {
return $this->commit('replace', $arguments, function ($file, $upload) use ($move) {
// delete all public versions
$file->unpublish(true);
// only move the original source if intended
$method = $move === true ? 'move' : 'copy';
// overwrite the original
if (F::copy($upload->root(), $file->root(), true) !== true) {
if (F::$method($upload->root(), $file->root(), true) !== true) {
throw new LogicException('The file could not be created');
}

View File

@@ -57,16 +57,17 @@ trait HasFiles
* Creates a new file
*
* @param array $props
* @param bool $move If set to `true`, the source will be deleted
* @return \Kirby\Cms\File
*/
public function createFile(array $props)
public function createFile(array $props, bool $move = false)
{
$props = array_merge($props, [
'parent' => $this,
'url' => null
]);
return File::create($props);
return File::create($props, $move);
}
/**

View File

@@ -26,7 +26,10 @@ class Helpers
*/
public static function deprecated(string $message): bool
{
if (App::instance()->option('debug') === true) {
if (
App::instance()->option('debug') === true ||
(defined('KIRBY_TESTING') === true && KIRBY_TESTING === true)
) {
return trigger_error($message, E_USER_DEPRECATED) === true;
}

View File

@@ -45,7 +45,8 @@ class Html extends \Kirby\Toolkit\Html
}
}
// only valid value for 'rel' is 'alternate stylesheet', if 'title' is given as well
// only valid value for 'rel' is 'alternate stylesheet',
// if 'title' is given as well
if (
($options['rel'] ?? '') !== 'alternate stylesheet' ||
($options['title'] ?? '') === ''

View File

@@ -658,12 +658,8 @@ class Language extends Model
*/
public function url(): string
{
$url = $this->url;
if ($url === null) {
$url = '/' . $this->code;
}
$url = $this->url;
$url ??= '/' . $this->code;
return Url::makeAbsolute($url, $this->kirby()->url());
}

View File

@@ -629,13 +629,11 @@ abstract class ModelWithContent extends Model implements Identifiable
]);
// validate the input
if ($validate === true) {
if ($form->isInvalid() === true) {
throw new InvalidArgumentException([
'fallback' => 'Invalid form with errors',
'details' => $form->errors()
]);
}
if ($validate === true && $form->isInvalid() === true) {
throw new InvalidArgumentException([
'fallback' => 'Invalid form with errors',
'details' => $form->errors()
]);
}
$arguments = [static::CLASS_ALIAS => $this, 'values' => $form->data(), 'strings' => $form->strings(), 'languageCode' => $languageCode];

View File

@@ -96,7 +96,7 @@ class Page extends ModelWithContent
* The template, that should be loaded
* if it exists
*
* @var \Kirby\Cms\Template
* @var \Kirby\Template\Template
*/
protected $intendedTemplate;
@@ -143,7 +143,7 @@ class Page extends ModelWithContent
/**
* The intended page template
*
* @var \Kirby\Cms\Template
* @var \Kirby\Template\Template
*/
protected $template;
@@ -504,7 +504,7 @@ class Page extends ModelWithContent
* Returns the template that should be
* loaded if it exists.
*
* @return \Kirby\Cms\Template
* @return \Kirby\Template\Template
*/
public function intendedTemplate()
{
@@ -1096,7 +1096,7 @@ class Page extends ModelWithContent
/**
* @internal
* @param mixed $type
* @return \Kirby\Cms\Template
* @return \Kirby\Template\Template
* @throws \Kirby\Exception\NotFoundException If the content representation cannot be found
*/
public function representation($type)
@@ -1277,13 +1277,13 @@ class Page extends ModelWithContent
public function slug(string $languageCode = null): string
{
if ($this->kirby()->multilang() === true) {
if ($languageCode === null) {
$languageCode = $this->kirby()->languageCode();
}
$languageCode ??= $this->kirby()->languageCode();
$defaultLanguageCode = $this->kirby()->defaultLanguage()->code();
if ($languageCode !== $defaultLanguageCode && $translation = $this->translations()->find($languageCode)) {
if (
$languageCode !== $defaultLanguageCode &&
$translation = $this->translations()->find($languageCode)
) {
return $translation->slug() ?? $this->slug;
}
}
@@ -1313,7 +1313,7 @@ class Page extends ModelWithContent
/**
* Returns the final template
*
* @return \Kirby\Cms\Template
* @return \Kirby\Template\Template
*/
public function template()
{

View File

@@ -223,7 +223,7 @@ trait PageActions
throw new InvalidArgumentException('Use the changeSlug method to change the slug for the default language');
}
$arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $languageCode];
$arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $language->code()];
return $this->commit('changeSlug', $arguments, function ($page, $slug, $languageCode) {
// remove the slug if it's the same as the folder name
if ($slug === $page->uid()) {
@@ -620,9 +620,7 @@ trait PageActions
->count();
// default positioning at the end
if ($num === null) {
$num = $max;
}
$num ??= $max;
// avoid zeros or negative numbers
if ($num < 1) {

View File

@@ -255,9 +255,7 @@ class Plugin extends Model
}
}
if ($option === null) {
$option = $kirby->option('updates') ?? true;
}
$option ??= $kirby->option('updates') ?? true;
if ($option !== true) {
return null;

View File

@@ -31,63 +31,37 @@ use Throwable;
*/
class System
{
/**
* @var \Kirby\Cms\App
*/
protected $app;
// cache
protected UpdateStatus|null $updateStatus = null;
/**
* @param \Kirby\Cms\App $app
*/
public function __construct(App $app)
public function __construct(protected App $app)
{
$this->app = $app;
// try to create all folders that could be missing
$this->init();
}
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
return $this->toArray();
}
/**
* Check for a writable accounts folder
*
* @return bool
*/
public function accounts(): bool
{
return is_writable($this->app->root('accounts'));
return is_writable($this->app->root('accounts')) === true;
}
/**
* Check for a writable content folder
*
* @return bool
*/
public function content(): bool
{
return is_writable($this->app->root('content'));
return is_writable($this->app->root('content')) === true;
}
/**
* Check for an existing curl extension
*
* @return bool
*/
public function curl(): bool
{
return extension_loaded('curl');
return extension_loaded('curl') === true;
}
/**
@@ -96,7 +70,6 @@ class System
* root. Otherwise it will return null.
*
* @param string $folder 'git', 'content', 'site', 'kirby'
* @return string|null
*/
public function exposedFileUrl(string $folder): string|null
{
@@ -142,19 +115,20 @@ class System
* root. Otherwise it will return null.
*
* @param string $folder 'git', 'content', 'site', 'kirby'
* @return string|null
*/
public function folderUrl(string $folder): string|null
{
$index = $this->app->root('index');
$root = match ($folder) {
'git' => $index . '/.git',
default => $this->app->root($folder)
};
if ($folder === 'git') {
$root = $index . '/.git';
} else {
$root = $this->app->root($folder);
}
if ($root === null || is_dir($root) === false || is_dir($index) === false) {
if (
$root === null ||
is_dir($root) === false ||
is_dir($index) === false
) {
return null;
}
@@ -180,22 +154,22 @@ class System
/**
* Returns the app's human-readable
* index URL without scheme
*
* @return string
*/
public function indexUrl(): string
{
return $this->app->url('index', true)->setScheme(null)->setSlash(false)->toString();
return $this->app->url('index', true)
->setScheme(null)
->setSlash(false)
->toString();
}
/**
* Create the most important folders
* if they don't exist yet
*
* @return void
* @throws \Kirby\Exception\PermissionException
*/
public function init()
public function init(): void
{
// init /site/accounts
try {
@@ -231,18 +205,16 @@ class System
* On a public server the panel.install
* option must be explicitly set to true
* to get the installer up and running.
*
* @return bool
*/
public function isInstallable(): bool
{
return $this->isLocal() === true || $this->app->option('panel.install', false) === true;
return
$this->isLocal() === true ||
$this->app->option('panel.install', false) === true;
}
/**
* Check if Kirby is already installed
*
* @return bool
*/
public function isInstalled(): bool
{
@@ -251,8 +223,6 @@ class System
/**
* Check if this is a local installation
*
* @return bool
*/
public function isLocal(): bool
{
@@ -261,8 +231,6 @@ class System
/**
* Check if all tests pass
*
* @return bool
*/
public function isOk(): bool
{
@@ -311,7 +279,9 @@ class System
$pubKey = F::read($this->app->root('kirby') . '/kirby.pub');
// verify the license signature
if (openssl_verify(json_encode($data), hex2bin($license['signature']), $pubKey, 'RSA-SHA256') !== 1) {
$data = json_encode($data);
$signature = hex2bin($license['signature']);
if (openssl_verify($data, $signature, $pubKey, 'RSA-SHA256') !== 1) {
return false;
}
@@ -338,9 +308,7 @@ class System
*/
protected function licenseUrl(string $url = null): string
{
if ($url === null) {
$url = $this->indexUrl();
}
$url ??= $this->indexUrl();
// remove common "testing" subdomains as well as www.
// to ensure that installations of the same site have
@@ -371,8 +339,6 @@ class System
* Returns the configured UI modes for the login form
* with their respective options
*
* @return array
*
* @throws \Kirby\Exception\InvalidArgumentException If the configuration is invalid
* (only in debug mode)
*/
@@ -431,45 +397,38 @@ class System
/**
* Check for an existing mbstring extension
*
* @return bool
*/
public function mbString(): bool
{
return extension_loaded('mbstring');
return extension_loaded('mbstring') === true;
}
/**
* Check for a writable media folder
*
* @return bool
*/
public function media(): bool
{
return is_writable($this->app->root('media'));
return is_writable($this->app->root('media')) === true;
}
/**
* Check for a valid PHP version
*
* @return bool
*/
public function php(): bool
{
return
version_compare(PHP_VERSION, '8.0.0', '>=') === true &&
version_compare(PHP_VERSION, '8.2.0', '<') === true;
version_compare(PHP_VERSION, '8.3.0', '<') === true;
}
/**
* Returns a sorted collection of all
* installed plugins
*
* @return \Kirby\Cms\Collection
*/
public function plugins()
public function plugins(): Collection
{
return (new Collection(App::instance()->plugins()))->sortBy('name', 'asc');
$plugins = new Collection($this->app->plugins());
return $plugins->sortBy('name', 'asc');
}
/**
@@ -477,24 +436,17 @@ class System
* and adds it to the .license file in the config
* folder if possible.
*
* @param string|null $license
* @param string|null $email
* @return bool
* @throws \Kirby\Exception\Exception
* @throws \Kirby\Exception\InvalidArgumentException
*/
public function register(string $license = null, string $email = null): bool
{
if (Str::startsWith($license, 'K3-PRO-') === false) {
throw new InvalidArgumentException([
'key' => 'license.format'
]);
throw new InvalidArgumentException(['key' => 'license.format']);
}
if (V::email($email) === false) {
throw new InvalidArgumentException([
'key' => 'license.email'
]);
throw new InvalidArgumentException(['key' => 'license.email']);
}
// @codeCoverageIgnoreStart
@@ -534,8 +486,6 @@ class System
/**
* Check for a valid server environment
*
* @return bool
*/
public function server(): bool
{
@@ -544,8 +494,6 @@ class System
/**
* Returns the detected server software
*
* @return string|null
*/
public function serverSoftware(): string|null
{
@@ -566,18 +514,14 @@ class System
/**
* Check for a writable sessions folder
*
* @return bool
*/
public function sessions(): bool
{
return is_writable($this->app->root('sessions'));
return is_writable($this->app->root('sessions')) === true;
}
/**
* Get an status array of all checks
*
* @return array
*/
public function status(): array
{
@@ -597,23 +541,18 @@ class System
* Returns the site's title as defined in the
* content file or `site.yml` blueprint
* @since 3.6.0
*
* @return string
*/
public function title(): string
{
$site = $this->app->site();
if ($site->title()->isNotEmpty()) {
if ($site->title()->isNotEmpty() === true) {
return $site->title()->value();
}
return $site->blueprint()->title();
}
/**
* @return array
*/
public function toArray(): array
{
return $this->status();
@@ -633,7 +572,9 @@ class System
}
$kirby = $this->app;
$option = $kirby->option('updates.kirby') ?? $kirby->option('updates') ?? true;
$option =
$kirby->option('updates.kirby') ??
$kirby->option('updates', true);
if ($option === false) {
return null;
@@ -648,11 +589,8 @@ class System
/**
* Upgrade to the new folder separator
*
* @param string $root
* @return void
*/
public static function upgradeContent(string $root)
public static function upgradeContent(string $root): void
{
$index = Dir::read($root);
@@ -666,4 +604,12 @@ class System
}
}
}
/**
* Improved `var_dump` output
*/
public function __debugInfo(): array
{
return $this->toArray();
}
}

View File

@@ -435,9 +435,7 @@ class UpdateStatus
// verify that we found at least one possible version;
// otherwise try the `$maxVersion` as a last chance before
// concluding at the top that we cannot solve the task
if ($incidentVersion === null) {
$incidentVersion = $maxVersion;
}
$incidentVersion ??= $maxVersion;
// we need a version that fixes all vulnerabilities, so use the
// "largest of the smallest" fixed versions

View File

@@ -10,6 +10,7 @@ use Kirby\Filesystem\F;
use Kirby\Panel\User as Panel;
use Kirby\Session\Session;
use Kirby\Toolkit\Str;
use SensitiveParameter;
/**
* The `$user` object represents a
@@ -274,11 +275,11 @@ class User extends ModelWithContent
* which will leave it as `null`
*
* @internal
* @param string|null $password
* @return string|null
*/
public static function hashPassword($password): string|null
{
public static function hashPassword(
#[SensitiveParameter]
string $password = null
): string|null {
if ($password !== null) {
$password = password_hash($password, PASSWORD_DEFAULT);
}
@@ -372,8 +373,9 @@ class User extends ModelWithContent
*/
public function isLastAdmin(): bool
{
return $this->role()->isAdmin() === true &&
$this->kirby()->users()->filter('role', 'admin')->count() <= 1;
return
$this->role()->isAdmin() === true &&
$this->kirby()->users()->filter('role', 'admin')->count() <= 1;
}
/**
@@ -410,12 +412,13 @@ class User extends ModelWithContent
/**
* Logs the user in
*
* @param string $password
* @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in
* @return bool
*/
public function login(string $password, $session = null): bool
{
public function login(
#[SensitiveParameter]
string $password,
$session = null
): bool {
$this->validatePassword($password);
$this->loginPasswordless($session);
@@ -750,11 +753,12 @@ class User extends ModelWithContent
/**
* Sets the user's password hash
*
* @param string $password|null
* @return $this
*/
protected function setPassword(string $password = null)
{
protected function setPassword(
#[SensitiveParameter]
string $password = null
): static {
$this->password = $password;
return $this;
}
@@ -848,15 +852,14 @@ class User extends ModelWithContent
/**
* Compares the given password with the stored one
*
* @param string $password|null
* @return bool
*
* @throws \Kirby\Exception\NotFoundException If the user has no password
* @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid
* or does not match the user password
*/
public function validatePassword(string $password = null): bool
{
public function validatePassword(
#[SensitiveParameter]
string $password = null
): bool {
if (empty($this->password()) === true) {
throw new NotFoundException(['key' => 'user.password.undefined']);
}

View File

@@ -11,6 +11,7 @@ use Kirby\Filesystem\F;
use Kirby\Form\Form;
use Kirby\Http\Idn;
use Kirby\Toolkit\Str;
use SensitiveParameter;
use Throwable;
/**
@@ -102,12 +103,11 @@ trait UserActions
/**
* Changes the user password
*
* @param string $password
* @return static
*/
public function changePassword(string $password)
{
public function changePassword(
#[SensitiveParameter]
string $password
): static {
return $this->commit('changePassword', ['user' => $this, 'password' => $password], function ($user, $password) {
$user = $user->clone([
'password' => $password = User::hashPassword($password)
@@ -379,12 +379,11 @@ trait UserActions
/**
* Writes the password to disk
*
* @param string|null $password
* @return bool
*/
protected function writePassword(string $password = null): bool
{
protected function writePassword(
#[SensitiveParameter]
string $password = null
): bool {
return F::write($this->root() . '/.htpasswd', $password);
}
}

View File

@@ -8,6 +8,7 @@ use Kirby\Exception\LogicException;
use Kirby\Exception\PermissionException;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
use SensitiveParameter;
/**
* Validators for all user actions
@@ -83,13 +84,13 @@ class UserRules
/**
* Validates if the password can be changed
*
* @param \Kirby\Cms\User $user
* @param string $password
* @return bool
* @throws \Kirby\Exception\PermissionException If the user is not allowed to change the password
*/
public static function changePassword(User $user, string $password): bool
{
public static function changePassword(
User $user,
#[SensitiveParameter]
string $password
): bool {
if ($user->permissions()->changePassword() !== true) {
throw new PermissionException([
'key' => 'user.changePassword.permission',
@@ -193,12 +194,13 @@ class UserRules
}
// check user permissions (if not on install)
if ($user->kirby()->users()->count() > 0) {
if ($user->permissions()->create() !== true) {
throw new PermissionException([
'key' => 'user.create.permission'
]);
}
if (
$user->kirby()->users()->count() > 0 &&
$user->permissions()->create() !== true
) {
throw new PermissionException([
'key' => 'user.create.permission'
]);
}
return true;
@@ -332,13 +334,13 @@ class UserRules
/**
* Validates a password
*
* @param \Kirby\Cms\User $user
* @param string $password
* @return bool
* @throws \Kirby\Exception\InvalidArgumentException If the password is too short
*/
public static function validPassword(User $user, string $password): bool
{
public static function validPassword(
User $user,
#[SensitiveParameter]
string $password
): bool {
if (Str::length($password ?? null) < 8) {
throw new InvalidArgumentException([
'key' => 'user.password.invalid',

View File

@@ -55,8 +55,9 @@ class Data
// find a handler or alias
$alias = static::$aliases[$type] ?? null;
$handler = static::$handlers[$type] ??
($alias ? static::$handlers[$alias] ?? null : null);
$handler =
static::$handlers[$type] ??
($alias ? static::$handlers[$alias] ?? null : null);
if ($handler === null || class_exists($handler) === false) {
throw new Exception('Missing handler for type: "' . $type . '"');

View File

@@ -6,6 +6,7 @@ use Kirby\Blueprint\Node;
use Kirby\Cms\ModelWithContent;
use Kirby\Option\Options;
use Kirby\Option\OptionsApi;
use Kirby\Option\OptionsProvider;
use Kirby\Option\OptionsQuery;
/**
@@ -20,7 +21,18 @@ use Kirby\Option\OptionsQuery;
class FieldOptions extends Node
{
public function __construct(
public Options|OptionsApi|OptionsQuery|null $options = null
/**
* The option source, either a fixed collection or
* a dynamic provider
*/
public Options|OptionsProvider|null $options = null,
/**
* Whether to escape special HTML characters in
* the option text for safe output in the Panel;
* only set to `false` if the text is later escaped!
*/
public bool $safeMode = true
) {
}
@@ -31,7 +43,7 @@ class FieldOptions extends Node
return parent::defaults();
}
public static function factory(array $props): static
public static function factory(array $props, bool $safeMode = true): static
{
$options = match ($props['type']) {
'api' => OptionsApi::factory($props),
@@ -39,20 +51,23 @@ class FieldOptions extends Node
default => Options::factory($props['options'] ?? [])
};
return new static($options);
return new static($options, $safeMode);
}
public static function polyfill(array $props = []): array
{
if (is_string($props['options'] ?? null) === true) {
$props['options'] = match ($props['options']) {
'api' => ['type' => 'api'] +
OptionsApi::polyfill($props['api'] ?? null),
'api' =>
['type' => 'api'] +
OptionsApi::polyfill($props['api'] ?? null),
'query' => ['type' => 'query'] +
OptionsQuery::polyfill($props['query'] ?? null),
'query' =>
['type' => 'query'] +
OptionsQuery::polyfill($props['query'] ?? null),
default => [ 'type' => 'query', 'query' => $props['options']]
default =>
[ 'type' => 'query', 'query' => $props['options']]
};
}
@@ -82,8 +97,8 @@ class FieldOptions extends Node
return $this->options;
}
// resolve OptionsApi or OptionsQuery to Options
return $this->options = $this->options->resolve($model);
// resolve OptionsProvider (OptionsApi or OptionsQuery) to Options
return $this->options = $this->options->resolve($model, $this->safeMode);
}
public function render(ModelWithContent $model): array

View File

@@ -48,12 +48,16 @@ class Dir
/**
* Copy the directory to a new destination
*
* @param array|false $ignore List of full paths to skip during copying
* or `false` to copy all files, including
* those listed in `Dir::$ignore`
*/
public static function copy(
string $dir,
string $target,
bool $recursive = true,
array $ignore = []
array|bool $ignore = []
): bool {
if (is_dir($dir) === false) {
throw new Exception('The directory "' . $dir . '" does not exist');
@@ -67,10 +71,13 @@ class Dir
throw new Exception('The target directory "' . $target . '" could not be created');
}
foreach (static::read($dir) as $name) {
foreach (static::read($dir, $ignore === false ? [] : null) as $name) {
$root = $dir . '/' . $name;
if (in_array($root, $ignore) === true) {
if (
is_array($ignore) === true &&
in_array($root, $ignore) === true
) {
continue;
}

View File

@@ -514,7 +514,14 @@ class F
static::remove($newRoot);
}
// actually move the file if it exists
$directory = dirname($newRoot);
// create the parent directory if it does not exist
if (is_dir($directory) === false) {
Dir::make($directory, true);
}
// actually move the file
if (rename($oldRoot, $newRoot) !== true) {
return false;
}

View File

@@ -62,7 +62,10 @@ class Field extends Component
public function __construct(string $type, array $attrs = [], ?Fields $formFields = null)
{
if (isset(static::$types[$type]) === false) {
throw new InvalidArgumentException('The field type "' . $type . '" does not exist');
throw new InvalidArgumentException([
'key' => 'field.type.missing',
'data' => ['name' => $attrs['name'] ?? '-', 'type' => $type]
]);
}
if (isset($attrs['model']) === false) {

View File

@@ -324,9 +324,7 @@ class Form
return $fields;
}
if ($language === null) {
$language = $kirby->language()->code();
}
$language ??= $kirby->language()->code();
if ($language !== $kirby->defaultLanguage()->code()) {
foreach ($fields as $fieldName => $fieldProps) {

View File

@@ -195,7 +195,7 @@ class Environment
* Sets the host name, port, path and protocol from the
* fixed list of allowed URLs
*/
protected function detectAllowed(array|string|object $allowed): void
protected function detectAllowed(array|string $allowed): void
{
$allowed = A::wrap($allowed);

View File

@@ -2,6 +2,8 @@
namespace Kirby\Http\Request;
use SensitiveParameter;
/**
* Base class for auth types
*
@@ -22,8 +24,10 @@ abstract class Auth
/**
* Constructor
*/
public function __construct(string $data)
{
public function __construct(
#[SensitiveParameter]
string $data
) {
$this->data = $data;
}

View File

@@ -4,6 +4,7 @@ namespace Kirby\Http\Request\Auth;
use Kirby\Http\Request\Auth;
use Kirby\Toolkit\Str;
use SensitiveParameter;
/**
* HTTP basic authentication data
@@ -20,8 +21,10 @@ class BasicAuth extends Auth
protected string|null $password;
protected string|null $username;
public function __construct(string $data)
{
public function __construct(
#[SensitiveParameter]
string $data
) {
parent::__construct($data);
$this->credentials = base64_decode($data);

View File

@@ -22,7 +22,7 @@ class Files
/**
* Sanitized array of all received files
*/
protected array $files;
protected array $files = [];
/**
* Creates a new Files object
@@ -31,11 +31,7 @@ class Files
*/
public function __construct(array|null $files = null)
{
if ($files === null) {
$files = $_FILES;
}
$this->files = [];
$files ??= $_FILES;
foreach ($files as $key => $file) {
if (is_array($file['name'])) {

View File

@@ -178,6 +178,7 @@ class Response
* @since 3.7.0
*
* @codeCoverageIgnore
* @todo Change return type to `never` once support for PHP 8.0 is dropped
*/
public static function go(string $url = '/', int $code = 302): void
{

View File

@@ -117,7 +117,7 @@ class Router
if ($callback) {
$result = $callback($route);
} else {
$result = $route?->action()->call($route, ...$route->arguments());
$result = $route->action()->call($route, ...$route->arguments());
}
$loop = false;
@@ -150,7 +150,7 @@ class Router
* find matches and return all the found
* arguments in the path.
*/
public function find(string $path, string $method, array|null $ignore = null): Route|null
public function find(string $path, string $method, array|null $ignore = null): Route
{
if (isset($this->routes[$method]) === false) {
throw new InvalidArgumentException('Invalid routing method: ' . $method, 400);

View File

@@ -5,6 +5,7 @@ namespace Kirby\Http;
use Kirby\Cms\App;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\Properties;
use SensitiveParameter;
use Throwable;
/**
@@ -326,8 +327,10 @@ class Uri
/**
* @return $this
*/
public function setPassword(string|null $password = null): static
{
public function setPassword(
#[SensitiveParameter]
string|null $password = null
): static {
$this->password = $password;
return $this;
}

View File

@@ -2,6 +2,8 @@
namespace Kirby\Image;
use Kirby\Toolkit\Str;
/**
* The Dimension class is used to provide additional
* methods for images and possibly other objects with
@@ -253,12 +255,28 @@ class Dimensions
if ($xml !== false) {
$attr = $xml->attributes();
$width = (int)($attr->width);
$height = (int)($attr->height);
if (($width === 0 || $height === 0) && empty($attr->viewBox) === false) {
$box = explode(' ', $attr->viewBox);
$width = (int)($box[2] ?? 0);
$height = (int)($box[3] ?? 0);
$rawWidth = $attr->width;
$width = (int)$rawWidth;
$rawHeight = $attr->height;
$height = (int)$rawHeight;
// use viewbox values if direct attributes are 0
// or based on percentages
if (empty($attr->viewBox) === false) {
$box = explode(' ', $attr->viewBox);
// when using viewbox values, make sure to subtract
// first two box values from last two box values
// to retrieve the absolute dimensions
if (Str::endsWith($rawWidth, '%') === true || $width === 0) {
$width = (int)($box[2] ?? 0) - (int)($box[0] ?? 0);
}
if (Str::endsWith($rawHeight, '%') === true || $height === 0) {
$height = (int)($box[3] ?? 0) - (int)($box[1] ?? 0);
}
}
}

View File

@@ -215,9 +215,10 @@ class Exif
*/
protected function parseFocalLength(): string|null
{
return $this->data['FocalLength'] ??
$this->data['FocalLengthIn35mmFilm'] ??
null;
return
$this->data['FocalLength'] ??
$this->data['FocalLengthIn35mmFilm'] ??
null;
}
/**

View File

@@ -112,6 +112,12 @@ class Image extends File
*/
public function html(array $attr = []): string
{
// if no alt text explicitly provided,
// try to infer from model content file
if ($alt = $this->model?->alt()) {
$attr['alt'] ??= $alt;
}
if ($url = $this->url()) {
return Html::img($url, $attr);
}

View File

@@ -8,7 +8,7 @@ use Kirby\Blueprint\NodeText;
use Kirby\Cms\ModelWithContent;
/**
* Option for select fields, radio fields, etc
* Option for select fields, radio fields, etc.
*
* @package Kirby Option
* @author Bastian Allgeier <bastian@getkirby.com>
@@ -19,7 +19,7 @@ use Kirby\Cms\ModelWithContent;
class Option
{
public function __construct(
public float|int|string|null $value,
public string|int|float|null $value,
public bool $disabled = false,
public NodeIcon|null $icon = null,
public NodeText|null $info = null,
@@ -28,7 +28,7 @@ class Option
$this->text ??= new NodeText(['en' => $this->value]);
}
public static function factory(float|int|string|null|array $props): static
public static function factory(string|int|float|null|array $props): static
{
if (is_array($props) === false) {
$props = ['value' => $props];

View File

@@ -6,7 +6,8 @@ use Kirby\Blueprint\Collection;
use Kirby\Cms\ModelWithContent;
/**
* Options
* Collection of possible options for
* select fields, radio fields, etc.
*
* @package Kirby Option
* @author Bastian Allgeier <bastian@getkirby.com>
@@ -30,6 +31,7 @@ class Options extends Collection
$collection = new static();
foreach ($items as $key => $option) {
// convert an associative value => text array into props;
// skip if option is already an array of option props
if (
is_array($option) === false ||

View File

@@ -88,8 +88,12 @@ class OptionsApi extends OptionsProvider
* Creates the actual options by loading
* data from the API and resolving it to
* the correct text-value entries
*
* @param bool $safeMode Whether to escape special HTML characters in
* the option text for safe output in the Panel;
* only set to `false` if the text is later escaped!
*/
public function resolve(ModelWithContent $model): Options
public function resolve(ModelWithContent $model, bool $safeMode = true): Options
{
// use cached options if present
// @codeCoverageIgnoreStart
@@ -101,25 +105,48 @@ class OptionsApi extends OptionsProvider
// apply property defaults
$this->defaults();
// load data from URL and narrow down to queried part
// load data from URL and convert from JSON to array
$data = $this->load($model);
if ($data === null) {
throw new NotFoundException('Options could not be loaded from API: ' . $model->toSafeString($this->url));
}
// turn data into Nest so that it can be queried
$data = Nest::create($data);
$data = Query::factory($this->query)->resolve($data);
// optionally query a substructure inside the data array
if ($this->query !== null) {
// turn data into Nest so that it can be queried
$data = Nest::create($data);
// actually apply the query and turn the result back into an array
$data = Query::factory($this->query)->resolve($data)->toArray();
}
// create options by resolving text and value query strings
// for each item from the data
$options = $data->toArray(fn ($item) => [
// value is always a raw string
'value' => $model->toString($this->value, ['item' => $item]),
// text is only a raw string when using {< >}
'text' => $model->toSafeString($this->text, ['item' => $item]),
]);
$options = array_map(
function ($item, $key) use ($model, $safeMode) {
// convert simple `key: value` API data
if (is_string($item) === true) {
$item = [
'key' => $key,
'value' => $item
];
}
$safeMethod = $safeMode === true ? 'toSafeString' : 'toString';
return [
// value is always a raw string
'value' => $model->toString($this->value, ['item' => $item]),
// text is only a raw string when using {< >}
// or when the safe mode is explicitly disabled (select field)
'text' => $model->$safeMethod($this->text, ['item' => $item])
];
},
// separately pass values and keys to have the keys available in the callback
$data,
array_keys($data)
);
// create Options object and render this subsequently
return $this->options = Options::factory($options);

View File

@@ -26,5 +26,13 @@ abstract class OptionsProvider
return $this->resolve($model)->render($model);
}
abstract public function resolve(ModelWithContent $model): Options;
/**
* Dynamically determines the actual options and resolves
* them to the correct text-value entries
*
* @param bool $safeMode Whether to escape special HTML characters in
* the option text for safe output in the Panel;
* only set to `false` if the text is later escaped!
*/
abstract public function resolve(ModelWithContent $model, bool $safeMode = true): Options;
}

View File

@@ -131,8 +131,12 @@ class OptionsQuery extends OptionsProvider
* Creates the actual options by running
* the query on the model and resolving it to
* the correct text-value entries
*
* @param bool $safeMode Whether to escape special HTML characters in
* the option text for safe output in the Panel;
* only set to `false` if the text is later escaped!
*/
public function resolve(ModelWithContent $model): Options
public function resolve(ModelWithContent $model, bool $safeMode = true): Options
{
// use cached options if present
// @codeCoverageIgnoreStart
@@ -159,7 +163,7 @@ class OptionsQuery extends OptionsProvider
}
// create options array
$options = $result->toArray(function ($item) use ($model) {
$options = $result->toArray(function ($item) use ($model, $safeMode) {
// get defaults based on item type
[$alias, $text, $value] = $this->itemToDefaults($item);
$data = ['item' => $item, $alias => $item];
@@ -167,9 +171,10 @@ class OptionsQuery extends OptionsProvider
// value is always a raw string
$value = $model->toString($this->value ?? $value, $data);
// text is only a raw string when HTML prop
// is explicitly set to true
$text = $model->toSafeString($this->text ?? $text, $data);
// text is only a raw string when using {< >}
// or when the safe mode is explicitly disabled (select field)
$safeMethod = $safeMode === true ? 'toSafeString' : 'toString';
$text = $model->$safeMethod($this->text ?? $text, $data);
return compact('text', 'value');
});

View File

@@ -196,9 +196,10 @@ class File extends Model
'rtf' => 'blue-400'
];
return $extensions[$this->model->extension()] ??
$types[$this->model->type()] ??
parent::imageDefaults()['color'];
return
$extensions[$this->model->extension()] ??
$types[$this->model->type()] ??
parent::imageDefaults()['color'];
}
/**
@@ -237,9 +238,10 @@ class File extends Model
'md' => 'markdown'
];
return $extensions[$this->model->extension()] ??
$types[$this->model->type()] ??
'file';
return
$extensions[$this->model->extension()] ??
$types[$this->model->type()] ??
'file';
}
/**

View File

@@ -196,10 +196,7 @@ class Page extends Model
protected function imageSource(
string|null $query = null
): CmsFile|Asset|null {
if ($query === null) {
$query = 'page.image';
}
$query ??= 'page.image';
return parent::imageSource($query);
}
@@ -234,8 +231,9 @@ class Page extends Model
*/
public function position(): int
{
return $this->model->num() ??
$this->model->parentModel()->children()->listed()->not($this->model)->count() + 1;
return
$this->model->num() ??
$this->model->parentModel()->children()->listed()->not($this->model)->count() + 1;
}
/**

View File

@@ -38,10 +38,7 @@ class Site extends Model
protected function imageSource(
string|null $query = null
): CmsFile|Asset|null {
if ($query === null) {
$query = 'site.image';
}
$query ??= 'site.image';
return parent::imageSource($query);
}

View File

@@ -96,7 +96,7 @@ class Element
* Tries to find a single nested element by
* query and otherwise returns null
*/
public function find(string $query): Element|null
public function find(string $query): static|null
{
if ($result = $this->query($query)[0]) {
return new static($result);
@@ -107,6 +107,8 @@ class Element
/**
* Returns the inner HTML of the element
*
* @param array|null $marks List of allowed marks
*/
public function innerHtml(array|null $marks = null): string
{

View File

@@ -38,18 +38,23 @@ class Argument
$argument = trim(substr($argument, 1, -1));
}
// string with single or double quotes
// string with single quotes
if (
(
Str::startsWith($argument, '"') &&
Str::endsWith($argument, '"')
) || (
Str::startsWith($argument, "'") &&
Str::endsWith($argument, "'")
)
Str::startsWith($argument, "'") &&
Str::endsWith($argument, "'")
) {
$string = substr($argument, 1, -1);
$string = str_replace(['\"', "\'"], ['"', "'"], $string);
$string = str_replace("\'", "'", $string);
return new static($string);
}
// string with double quotes
if (
Str::startsWith($argument, '"') &&
Str::endsWith($argument, '"')
) {
$string = substr($argument, 1, -1);
$string = str_replace('\"', '"', $string);
return new static($string);
}

View File

@@ -6,8 +6,8 @@ use Kirby\Toolkit\A;
use Kirby\Toolkit\Collection;
/**
* The Argument class represents a single
* parameter passed to a method in a chained query
* The Arguments class helps splitting a
* parameter string into processable arguments
*
* @package Kirby Query
* @author Nico Hoffmann <nico@getkirby.com>
@@ -26,8 +26,9 @@ class Arguments extends Collection
// skip all matches inside of single quotes
public const NO_SLQU = '\'(?:[^\'\\\\]|\\\\.)*\'(*SKIP)(*FAIL)';
// skip all matches inside of any of the above skip groups
public const OUTSIDE = self::NO_PNTH . '|' . self::NO_SQBR . '|' .
self::NO_DLQU . '|' . self::NO_SLQU;
public const OUTSIDE =
self::NO_PNTH . '|' . self::NO_SQBR . '|' .
self::NO_DLQU . '|' . self::NO_SLQU;
/**
* Splits list of arguments into individual

View File

@@ -22,6 +22,9 @@ class Expression
) {
}
/**
* Parses an expression string into its parts
*/
public static function factory(string $expression, Query $parent = null): static|Segments
{
// split into different expression parts and operators

View File

@@ -124,7 +124,6 @@ Query::$entries['site'] = function (): Site {
return App::instance()->site();
};
Query::$entries['t'] = function (
string $key,
string|array $fallback = null,

View File

@@ -51,6 +51,11 @@ class Segment
throw new BadMethodCallException($error);
}
/**
* Parses a segment into the property/method name and its arguments
*
* @param int $position String position of the segment inside the full query
*/
public static function factory(
string $segment,
int $position = 0
@@ -69,6 +74,10 @@ class Segment
);
}
/**
* Automatically resolves the segment depending on the
* segment position and the type of the base
*/
public function resolve(mixed $base = null, array|object $data = []): mixed
{
// resolve arguments to array

View File

@@ -81,8 +81,9 @@ class Segments extends Collection
return null;
}
// for regular connectors, just skip
if ($segment === '.') {
// for regular connectors and optional chaining on non-null,
// just skip this connecting segment
if ($segment === '.' || $segment === '?.') {
continue;
}

View File

@@ -56,8 +56,9 @@ class Sane
// find a handler or alias
$alias = static::$aliases[$type] ?? null;
$handler = static::$handlers[$type] ??
($alias ? static::$handlers[$alias] ?? null : null);
$handler =
static::$handlers[$type] ??
($alias ? static::$handlers[$alias] ?? null : null);
if (empty($handler) === false && class_exists($handler) === true) {
return new $handler();

View File

@@ -339,7 +339,11 @@ class Session
* @todo The $this->destroyed check gets flagged by Psalm for unknown reasons
* @psalm-suppress ParadoxicalCondition
*/
if ($this->writeMode !== true || $this->tokenExpiry === null || $this->destroyed === true) {
if (
$this->writeMode !== true ||
$this->tokenExpiry === null ||
$this->destroyed === true
) {
return;
}
@@ -523,7 +527,11 @@ class Session
* @todo The $this->destroyed check gets flagged by Psalm for unknown reasons
* @psalm-suppress ParadoxicalCondition
*/
if ($this->tokenExpiry === null || $this->destroyed === true || $this->writeMode === true) {
if (
$this->tokenExpiry === null ||
$this->destroyed === true ||
$this->writeMode === true
) {
return;
}

123
kirby/src/Template/Slot.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
namespace Kirby\Template;
use Kirby\Exception\LogicException;
/**
* The slot class catches all content
* between the beginning and the end of
* a slot. Slot content is then stored
* in the Slots collection.
*
* @package Kirby Template
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Slot
{
/**
* The captured slot content
* @internal
*/
public string|null $content;
/**
* The name that was declared during
* the definition of the slot
*/
protected string $name;
/**
* Keeps track of the slot state
*/
protected bool $open = false;
/**
* Creates a new slot
*/
public function __construct(string $name, string|null $content = null)
{
$this->name = $name;
$this->content = $content;
}
/**
* Renders the slot content or an empty string
* if the slot is empty.
*/
public function __toString(): string
{
return $this->render() ?? '';
}
/**
* Used in the slot helper
*/
public static function begin(string $name = 'default'): static|null
{
return Snippet::$current?->slot($name);
}
/**
* Closes a slot and catches all the content
* that has been printed since the slot has
* been opened
*/
public function close(): void
{
if ($this->open === false) {
throw new LogicException('The slot has not been opened');
}
$this->content = ob_get_clean();
$this->open = false;
}
/**
* Used in the endslot() helper
*/
public static function end(): void
{
Snippet::$current?->endslot();
}
/**
* Returns whether the slot is currently
* open and being buffered
*/
public function isOpen(): bool
{
return $this->open;
}
/**
* Returns the slot name
*/
public function name(): string
{
return $this->name;
}
/**
* Opens the slot and starts
* output buffering
*/
public function open(): void
{
$this->open = true;
// capture the output
ob_start();
}
/**
* Returns the slot content
*/
public function render(): string|null
{
return $this->content;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Kirby\Template;
use Countable;
/**
* The slots collection is simplifying
* slot access. Slots can be accessed with
* `$slots->heading()` and accessing a non-existing
* slot will simply return null.
*
* @package Kirby Template
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Slots implements Countable
{
/**
* Creates a new slots collection
*/
public function __construct(protected array $slots)
{
}
/**
* Magic getter for slots;
* e.g. `$slots->heading`
*/
public function __get(string $name): Slot|null
{
return $this->slots[$name] ?? null;
}
/**
* Magic getter method for slots;
* e.g. `$slots->heading()`
*/
public function __call(string $name, array $args): Slot|null
{
return $this->__get($name);
}
/**
* Counts the number of defined slots
*/
public function count(): int
{
return count($this->slots);
}
}

View File

@@ -0,0 +1,321 @@
<?php
namespace Kirby\Template;
use Kirby\Cms\App;
use Kirby\Cms\Helpers;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Tpl;
/**
* The Snippet class includes shared code parts
* in templates and allows to pass data as well as to
* optionally pass content to various predefined slots.
*
* @package Kirby Template
* @author Bastian Allgeier <bastian@getkirby.com>,
* Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Snippet extends Tpl
{
/**
* Cache for the currently active
* snippet. This is used to start
* and end slots within this snippet
* in the helper functions
* @internal
*/
public static self|null $current = null;
/**
* Contains all slots that are opened
* but not yet closed
*/
protected array $capture = [];
/**
* Associative array with variables that
* will be set inside the snippet
*/
protected array $data;
/**
* An empty dummy slots object used for snippets
* that were loaded without passing slots
*/
protected static Slots|null $dummySlots = null;
/**
* Full path to the PHP file of the snippet;
* can be `null` for "dummy" snippets that don't exist
*/
protected string|null $file;
/**
* Keeps track of the state of the snippet
*/
protected bool $open = false;
/**
* The parent snippet
*/
protected self|null $parent = null;
/**
* The collection of closed slots that will be used
* to pass down to the template for the snippet.
*/
protected array $slots = [];
/**
* Creates a new snippet
*/
public function __construct(string|null $file, array $data = [])
{
$this->file = $file;
$this->data = $data;
}
/**
* Creates and opens a new snippet. This can be used
* directly in a template or via the slots() helper
*/
public static function begin(string|null $file, array $data = []): static
{
$snippet = new static($file, $data);
return $snippet->open();
}
/**
* Closes the snippet and catches
* the default slot if no slots have been
* defined in between opening and closing.
*/
public function close(): static
{
// make sure that ending a snippet
// is only supported if the snippet has
// been started before
if ($this->open === false) {
throw new LogicException('The snippet has not been opened');
}
// create a default slot for the content
// that has been captured between start and end
if (empty($this->slots) === true) {
$this->slots['default'] = new Slot('default');
$this->slots['default']->content = ob_get_clean();
} else {
// swallow any "unslotted" content
// between start and end
ob_end_clean();
}
$this->open = false;
// switch back to the parent in nested
// snippet stacks
static::$current = $this->parent;
return $this;
}
/**
* Used in the endsnippet() helper
*/
public static function end(): void
{
echo static::$current?->render();
}
/**
* Closes the last openend slot
*/
public function endslot(): void
{
// take the last slot from the capture stack
$slot = array_pop($this->capture);
// capture the content and close the slot
$slot->close();
// add the slot to the scope
$this->slots[$slot->name()] = $slot;
}
/**
* Returns either an open snippet capturing slots
* or the template string for self-enclosed snippets
*/
public static function factory(
string|array $name,
array $data = [],
bool $slots = false
): static|string {
$file = static::file($name);
// for snippets with slots, make sure to open a new
// snippet and start capturing slots
if ($slots === true) {
return static::begin($file, $data);
}
// for snippets without slots, directly load and return
// the snippet's template file
return static::load($file, static::scope($data));
}
/**
* Absolute path to the file for
* the snippet/s taking snippets defined in plugins
* into account
*/
public static function file(string|array $name): string|null
{
$kirby = App::instance();
$root = static::root();
$names = A::wrap($name);
foreach ($names as $name) {
$name = (string)$name;
$file = $root . '/' . $name . '.php';
if (file_exists($file) === false) {
$file = $kirby->extensions('snippets')[$name] ?? null;
}
if ($file) {
break;
}
}
return $file;
}
/**
* Opens the snippet and starts output
* buffering to catch all slots in between
*/
public function open(): static
{
if (static::$current !== null) {
$this->parent = static::$current;
}
$this->open = true;
static::$current = $this;
ob_start();
return $this;
}
/**
* Returns the parent snippet if it exists
*/
public function parent(): static|null
{
return $this->parent;
}
/**
* Renders the snippet and passes the scope
* with all slots and data
*/
public function render(array $data = [], array $slots = []): string
{
// always make sure that the snippet
// is closed before it can be rendered
if ($this->open === true) {
$this->close();
}
// manually add slots
foreach ($slots as $slotName => $slotContent) {
$this->slots[$slotName] = new Slot($slotName, $slotContent);
}
// custom data overrides for the data that was passed to the snippet instance
$data = array_replace_recursive($this->data, $data);
return static::load($this->file, static::scope($data, $this->slots()));
}
/**
* Returns the root directory for all
* snippet templates
*/
public static function root(): string
{
return App::instance()->root('snippets');
}
/**
* Starts a new slot with the given name
*/
public function slot(string $name = 'default'): Slot
{
$slot = new Slot($name);
$slot->open();
// start a new slot
$this->capture[] = $slot;
return $slot;
}
/**
* Returns the slots collection
*/
public function slots(): Slots
{
return new Slots($this->slots);
}
/**
* Returns the data variables that get passed to a snippet
*
* @param \Kirby\Template\Slots|null $slots If null, an empty dummy object is used
*/
protected static function scope(array $data = [], Slots|null $slots = null): array
{
// initialize a dummy slots object and cache it for better performance
if ($slots === null) {
$slots = static::$dummySlots ??= new Slots([]);
}
$data = array_merge(App::instance()->data, $data);
// TODO 3.10: Replace the following code:
// if (
// array_key_exists('slot', $data) === true ||
// array_key_exists('slots', $data) === true
// ) {
// throw new InvalidArgumentException('Passing the $slot or $slots variables to snippets is not supported.');
// }
//
// return array_merge($data, [
// 'slot' => $slots->default,
// 'slots' => $slots,
// ]);
// @codeCoverageIgnoreStart
if (
array_key_exists('slot', $data) === true ||
array_key_exists('slots', $data) === true
) {
Helpers::deprecated('Passing the $slot or $slots variables to snippets is deprecated and will break in Kirby 3.10.');
}
// @codeCoverageIgnoreEnd
return array_merge([
'slot' => $slots->default,
'slots' => $slots,
], $data);
}
}

View File

@@ -1,8 +1,9 @@
<?php
namespace Kirby\Cms;
namespace Kirby\Template;
use Exception;
use Kirby\Cms\App;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Tpl;
@@ -10,7 +11,7 @@ use Kirby\Toolkit\Tpl;
* Represents a Kirby template and takes care
* of loading the correct file.
*
* @package Kirby Cms
* @package Kirby Template
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
@@ -20,38 +21,26 @@ class Template
{
/**
* Global template data
*
* @var array
*/
public static $data = [];
/**
* The name of the template
*
* @var string
*/
protected $name;
/**
* Template type (html, json, etc.)
*
* @var string
*/
protected $type;
public static array $data = [];
/**
* Default template type if no specific type is set
*
* @var string
*/
protected $defaultType;
protected string $defaultType;
/**
* The name of the template
*/
protected string $name;
/**
* Template type (html, json, etc.)
*/
protected string $type;
/**
* Creates a new template object
*
* @param string $name
* @param string $type
* @param string $defaultType
*/
public function __construct(string $name, string $type = 'html', string $defaultType = 'html')
{
@@ -63,18 +52,22 @@ class Template
/**
* Converts the object to a simple string
* This is used in template filters for example
*
* @return string
*/
public function __toString(): string
{
return $this->name;
}
/**
* Returns the default template type
*/
public function defaultType(): string
{
return $this->defaultType;
}
/**
* Checks if the template exists
*
* @return bool
*/
public function exists(): bool
{
@@ -87,75 +80,61 @@ class Template
/**
* Returns the expected template file extension
*
* @return string
*/
public function extension(): string
{
return 'php';
}
/**
* Returns the default template type
*
* @return string
*/
public function defaultType(): string
{
return $this->defaultType;
}
/**
* Returns the place where templates are located
* in the site folder and and can be found in extensions
*
* @return string
*/
public function store(): string
{
return 'templates';
}
/**
* Detects the location of the template file
* if it exists.
*
* @return string|null
*/
public function file(): string|null
{
$name = $this->name();
$extension = $this->extension();
$store = $this->store();
$root = $this->root();
if ($this->hasDefaultType() === true) {
try {
// Try the default template in the default template directory.
return F::realpath($this->root() . '/' . $this->name() . '.' . $this->extension(), $this->root());
return F::realpath($root . '/' . $name . '.' . $extension, $root);
} catch (Exception) {
// ignore errors, continue searching
}
// Look for the default template provided by an extension.
$path = App::instance()->extension($this->store(), $this->name());
$path = App::instance()->extension($store, $name);
if ($path !== null) {
return $path;
}
}
$name = $this->name() . '.' . $this->type();
$name .= '.' . $this->type();
try {
// Try the template with type extension in the default template directory.
return F::realpath($this->root() . '/' . $name . '.' . $this->extension(), $this->root());
return F::realpath($root . '/' . $name . '.' . $extension, $root);
} catch (Exception) {
// Look for the template with type extension provided by an extension.
// This might be null if the template does not exist.
return App::instance()->extension($this->store(), $name);
return App::instance()->extension($store, $name);
}
}
/**
* Checks if the template uses the default type
*/
public function hasDefaultType(): bool
{
return $this->type() === $this->defaultType();
}
/**
* Returns the template name
*
* @return string
*/
public function name(): string
{
@@ -163,43 +142,67 @@ class Template
}
/**
* @param array $data
* @return string
* Renders the template with the given template data
*/
public function render(array $data = []): string
{
return Tpl::load($this->file(), $data);
// if the template is rendered inside a snippet,
// we need to keep the "outside" snippet object
// to compare it later
$snippet = Snippet::$current;
// load the template
$template = Tpl::load($this->file(), $data);
// if last `endsnippet()` inside the current template
// has been omitted (= snippet was used as layout snippet),
// `Snippet::$current` will point to a snippet that was
// opened inside the template; if that snippet is the direct
// child of the snippet that was open before the template was
// rendered (which could be `null` if no snippet was open),
// take the buffer output from the template as default slot
// and render the snippet as final template output
if (
Snippet::$current === null ||
Snippet::$current->parent() !== $snippet
) {
return $template;
}
// no slots have been defined, but the template code
// should be used as default slot
if (Snippet::$current->slots()->count() === 0) {
return Snippet::$current->render($data, [
'default' => $template
]);
}
// let the snippet close and render natively
return Snippet::$current->render($data);
}
/**
* Returns the root to the templates directory
*
* @return string
*/
public function root(): string
{
return App::instance()->root($this->store());
}
/**
* Returns the place where templates are located
* in the site folder and and can be found in extensions
*/
public function store(): string
{
return 'templates';
}
/**
* Returns the template type
*
* @return string
*/
public function type(): string
{
return $this->type;
}
/**
* Checks if the template uses the default type
*
* @return bool
*/
public function hasDefaultType(): bool
{
$type = $this->type();
return $type === null || $type === $this->defaultType();
}
}

View File

@@ -148,6 +148,7 @@ class KirbyTag
return $this->kirby()->file($path, null, true);
}
/**
* Returns the current Kirby instance
*/

View File

@@ -25,6 +25,9 @@ class KirbyTags
array $data = [],
array $options = []
): string {
// make sure $text is a string
$text ??= '';
$regex = '!
(?=[^\]]) # positive lookahead that matches a group after the main expression without including ] in the result
(?=\([a-z0-9_-]+:) # positive lookahead that requires starts with ( and lowercase ASCII letters, digits, underscores or hyphens followed with : immediately to the right of the current location
@@ -40,7 +43,10 @@ class KirbyTags
return KirbyTag::parse($match[0], $data, $options)->render();
} catch (InvalidArgumentException $e) {
// stay silent in production and ignore non-existing tags
if ($debug !== true || Str::startsWith($e->getMessage(), 'Undefined tag type:') === true) {
if (
$debug !== true ||
Str::startsWith($e->getMessage(), 'Undefined tag type:') === true
) {
return $match[0];
}
@@ -52,6 +58,6 @@ class KirbyTags
return $match[0];
}
}, $text ?? '');
}, $text);
}
}

View File

@@ -53,11 +53,10 @@ class Markdown
*/
public function parse(string|null $text = null, bool $inline = false): string
{
if ($this->options['extra'] === true) {
$parser = new ParsedownExtra();
} else {
$parser = new Parsedown();
}
$parser = match ($this->options['extra']) {
true => new ParsedownExtra(),
default => new Parsedown()
};
$parser->setBreaksEnabled($this->options['breaks']);
$parser->setSafeMode($this->options['safe']);

View File

@@ -110,7 +110,8 @@ class SmartyPants
public function parse(string|null $text = null): string
{
// prepare the text
$text = str_replace('&quot;', '"', $text ?? '');
$text ??= '';
$text = str_replace('&quot;', '"', $text);
// parse the text
return $this->parser->transform($text);

View File

@@ -114,9 +114,16 @@ class A
// the element needs to exist and also needs to be an array; otherwise
// we cannot find the remaining keys within it (invalid array structure)
if (isset($array[$currentKey]) === true && is_array($array[$currentKey]) === true) {
if (
isset($array[$currentKey]) === true &&
is_array($array[$currentKey]) === true
) {
// $keys only holds the remaining keys that have not been shifted off yet
return static::get($array[$currentKey], implode('.', $keys), $default);
return static::get(
$array[$currentKey],
implode('.', $keys),
$default
);
}
}
@@ -127,7 +134,11 @@ class A
// if the input array uses a completely nested structure,
// recursively progress layer by layer
if (is_array($array[$firstKey]) === true) {
return static::get($array[$firstKey], implode('.', $keys), $default);
return static::get(
$array[$firstKey],
implode('.', $keys),
$default
);
}
// the $firstKey element was found, but isn't an array, so we cannot
@@ -146,6 +157,7 @@ class A
if (is_string($value) === true) {
return $value;
}
return implode($separator, $value);
}
@@ -251,6 +263,7 @@ class A
public static function pluck(array $array, string $key): array
{
$output = [];
foreach ($array as $a) {
if (isset($a[$key]) === true) {
$output[] = $a[$key];
@@ -396,12 +409,12 @@ class A
*/
public static function fill(array $array, int $limit, $fill = 'placeholder'): array
{
if (count($array) < $limit) {
$diff = $limit - count($array);
for ($x = 0; $x < $diff; $x++) {
$array[] = $fill;
}
$diff = $limit - count($array);
for ($x = 0; $x < $diff; $x++) {
$array[] = $fill;
}
return $array;
}
@@ -467,13 +480,7 @@ class A
*/
public static function missing(array $array, array $required = []): array
{
$missing = [];
foreach ($required as $r) {
if (isset($array[$r]) === false) {
$missing[] = $r;
}
}
return $missing;
return array_values(array_diff($required, array_keys($array)));
}
/**

View File

@@ -1464,7 +1464,8 @@ Collection::$filters['date <='] = [
*/
Collection::$filters['date between'] = Collection::$filters['date ..'] = [
'validator' => function ($value, $test) {
return V::date($value, '>=', $test[0]) &&
V::date($value, '<=', $test[1]);
return
V::date($value, '>=', $test[0]) &&
V::date($value, '<=', $test[1]);
}
];

View File

@@ -19,24 +19,18 @@ use ReflectionFunction;
*/
class Controller
{
protected $function;
public function __construct(Closure $function)
public function __construct(protected Closure $function)
{
$this->function = $function;
}
public function arguments(array $data = []): array
{
$info = new ReflectionFunction($this->function);
$args = [];
foreach ($info->getParameters() as $parameter) {
$name = $parameter->getName();
$args[] = $data[$name] ?? null;
}
return $args;
return A::map(
$info->getParameters(),
fn ($parameter) => $data[$parameter->getName()] ?? null
);
}
public function call($bind = null, $data = [])
@@ -44,7 +38,7 @@ class Controller
$args = $this->arguments($data);
if ($bind === null) {
return call_user_func($this->function, ...$args);
return ($this->function)(...$args);
}
return $this->function->call($bind, ...$args);

View File

@@ -435,10 +435,7 @@ class Dom
{
$allowedNamespaces = $options['allowedNamespaces'];
$localName = $node->localName;
if ($compare === null) {
$compare = fn ($expected, $real): bool => $expected === $real;
}
$compare ??= fn ($expected, $real): bool => $expected === $real;
// if the configuration does not define namespace URIs or if the
// currently checked node is from the special `xml:` namespace
@@ -709,9 +706,7 @@ class Dom
// ensure that the document is encoded as UTF-8
// unless a different encoding was specified in
// the input or before exporting
if ($this->doc->encoding === null) {
$this->doc->encoding = 'UTF-8';
}
$this->doc->encoding ??= 'UTF-8';
return trim($this->doc->saveXML());
}

View File

@@ -25,10 +25,8 @@ class Escape
{
/**
* The internal singleton escaper instance
*
* @var \Laminas\Escaper\Escaper
*/
protected static $escaper;
protected static Escaper|null $escaper;
/**
* Escape common HTML attributes data
@@ -44,11 +42,8 @@ class Escape
* <div attr=...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...>content</div>
* <div attr='...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...'>content</div>
* <div attr="...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...">content</div>
*
* @param string $string
* @return string
*/
public static function attr($string)
public static function attr(string $string): string
{
return static::escaper()->escapeHtmlAttr($string);
}
@@ -65,21 +60,16 @@ class Escape
* <style>selector { property : ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...; } </style>
* <style>selector { property : "...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE..."; } </style>
* <span style="property : ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...">text</span>
*
* @param string $string
* @return string
*/
public static function css($string)
public static function css(string $string): string
{
return static::escaper()->escapeCss($string);
}
/**
* Get the escaper instance (and create if needed)
*
* @return \Laminas\Escaper\Escaper
*/
protected static function escaper()
protected static function escaper(): Escaper
{
return static::$escaper ??= new Escaper('utf-8');
}
@@ -95,11 +85,8 @@ class Escape
*
* <body>...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...</body>
* <div>...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...</div>
*
* @param string $string
* @return string
*/
public static function html($string)
public static function html(string $string): string
{
return static::escaper()->escapeHtml($string);
}
@@ -113,11 +100,8 @@ class Escape
* <script>alert('...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...')</script>
* <script>x='...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...'</script>
* <div onmouseover="x='...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...'"</div>
*
* @param string $string
* @return string
*/
public static function js($string)
public static function js(string $string): string
{
return static::escaper()->escapeJs($string);
}
@@ -129,11 +113,8 @@ class Escape
* This should not be used to escape an entire URI.
*
* <a href="http://www.somesite.com?test=...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...">link</a>
*
* @param string $string
* @return string
*/
public static function url($string)
public static function url(string $string): string
{
return rawurlencode($string);
}
@@ -151,11 +132,8 @@ class Escape
* & is replaced with &amp;
* < is replaced with &lt;
* > is replaced with &gt;
*
* @param string $string
* @return string
*/
public static function xml($string)
public static function xml(string $string): string
{
return htmlspecialchars($string, ENT_QUOTES | ENT_XML1, 'UTF-8');
}

View File

@@ -19,17 +19,13 @@ class Html extends Xml
{
/**
* An internal store for an HTML entities translation table
*
* @var array
*/
public static $entities;
public static array|null $entities;
/**
* List of HTML tags that can be used inline
*
* @var array
*/
public static $inlineList = [
public static array $inlineList = [
'b',
'i',
'small',
@@ -144,6 +140,11 @@ class Html extends Xml
return $value === true ? strtolower($name) : null;
}
// HTML attribute names are case-insensitive
if (is_string($name) === true) {
$name = strtolower($name);
}
// all other cases can share the XML variant
$attr = parent::attr($name, $value);
@@ -375,7 +376,7 @@ class Html extends Xml
return trim($rel . ' noopener noreferrer', ' ');
}
return $rel;
return $rel ?: null;
}
/**
@@ -389,13 +390,11 @@ class Html extends Xml
* @param int $level Indentation level
* @return string The generated HTML
*/
public static function tag(string $name, $content = '', array $attr = null, string $indent = null, int $level = 0): string
public static function tag(string $name, $content = '', array $attr = [], string $indent = null, int $level = 0): string
{
// treat an explicit `null` value as an empty tag
// as void tags are already covered below
if ($content === null) {
$content = '';
}
$content ??= '';
// force void elements to be self-closing
if (static::isVoid($name) === true) {
@@ -629,7 +628,7 @@ class Html extends Xml
}
// build the full video src URL
$src = $src . $query->toString(true);
$src .= $query->toString(true);
// render the iframe
return static::iframe($src, static::videoAttr($attr));

View File

@@ -41,7 +41,7 @@ class I18n
* The fallback locale or a
* list of fallback locales
*
* @var string|array|\Closure
* @var string|array|\Closure|null
*/
public static $fallback = ['en'];
@@ -54,8 +54,6 @@ class I18n
/**
* Returns the list of fallback locales
*
* @return array
*/
public static function fallbacks(): array
{
@@ -77,9 +75,7 @@ class I18n
* Returns singular or plural
* depending on the given number
*
* @param int $count
* @param bool $none If true, 'none' will be returned if the count is 0
* @return string
*/
public static function form(int $count, bool $none = false): string
{
@@ -92,23 +88,17 @@ class I18n
/**
* Formats a number
*
* @param int|float $number
* @param string $locale
* @return string
*/
public static function formatNumber($number, string $locale = null): string
public static function formatNumber(int|float $number, string $locale = null): string
{
$locale ??= static::locale();
$formatter = static::decimalNumberFormatter($locale);
$number = $formatter?->format($number) ?? $number;
$locale ??= static::locale();
$formatter = static::decimalNumberFormatter($locale);
$number = $formatter?->format($number) ?? $number;
return (string)$number;
}
/**
* Returns the locale code
*
* @return string
*/
public static function locale(): string
{
@@ -126,25 +116,22 @@ class I18n
/**
* Translates a given message
* according to the currently set locale
*
* @param string|array $key
* @param string|array|null $fallback
* @param string|null $locale
* @return string|array|null
*/
public static function translate($key, $fallback = null, string $locale = null)
{
public static function translate(
string|array|null $key,
string|array $fallback = null,
string $locale = null
): string|array|Closure|null {
$locale ??= static::locale();
if (is_array($key) === true) {
// try to use actual locale
if (isset($key[$locale]) === true) {
return $key[$locale];
if ($result = $key[$locale] ?? null) {
return $result;
}
// try to use language code, e.g. `es` when locale is `es_ES`
$lang = Str::before($locale, '_');
if (isset($key[$lang]) === true) {
return $key[$lang];
if ($result = $key[Str::before($locale, '_')] ?? null) {
return $result;
}
// use global wildcard as i18n key
if (isset($key['*']) === true) {
@@ -152,16 +139,18 @@ class I18n
}
// use fallback
if (is_array($fallback) === true) {
return $fallback[$locale] ??
$fallback['en'] ??
reset($fallback);
return
$fallback[$locale] ??
$fallback['en'] ??
reset($fallback);
}
return $fallback;
}
if ($translation = static::translation($locale)[$key] ?? null) {
return $translation;
// $key is a string
if ($result = static::translation($locale)[$key] ?? null) {
return $result;
}
if ($fallback !== null) {
@@ -174,8 +163,8 @@ class I18n
continue;
}
if ($translation = static::translation($fallback)[$key] ?? null) {
return $translation;
if ($result = static::translation($fallback)[$key] ?? null) {
return $result;
}
}
@@ -192,8 +181,12 @@ class I18n
* @param string|null $locale
* @return string
*/
public static function template(string $key, $fallback = null, array|null $replace = null, string|null $locale = null): string
{
public static function template(
string $key,
string|array $fallback = null,
array|null $replace = null,
string|null $locale = null
): string {
if (is_array($fallback) === true) {
$replace = $fallback;
$fallback = null;
@@ -271,11 +264,7 @@ class I18n
* defined, the template that is defined last in the translation array is used
* - Translation is a callback with a `$count` argument: Returns the callback return value
*
* @param string $key
* @param int $count
* @param string|null $locale
* @param bool $formatNumber If set to `false`, the count is not formatted
* @return mixed
*/
public static function translateCount(string $key, int $count, string $locale = null, bool $formatNumber = true)
{

View File

@@ -16,23 +16,20 @@ use IteratorAggregate;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* @psalm-suppress MissingTemplateParam Implementing template params in this class would
* require implementing them throughout the code base
* https://github.com/getkirby/kirby/pull/4886#pullrequestreview-1203577545
* @psalm-suppress MissingTemplateParam Implementing template params
* in this class would require
* implementing them throughout
* the code base: https://github.com/getkirby/kirby/pull/4886#pullrequestreview-1203577545
*/
class Iterator implements IteratorAggregate
{
/**
* The data array
*
* @var array
*/
public $data = [];
public array $data = [];
/**
* Constructor
*
* @param array $data
*/
public function __construct(array $data = [])
{
@@ -41,8 +38,6 @@ class Iterator implements IteratorAggregate
/**
* Get an iterator for the items.
*
* @return \ArrayIterator
*/
public function getIterator(): ArrayIterator
{
@@ -51,18 +46,14 @@ class Iterator implements IteratorAggregate
/**
* Returns the current key
*
* @return string
*/
public function key()
public function key(): int|string|null
{
return key($this->data);
}
/**
* Returns an array of all keys
*
* @return array
*/
public function keys(): array
{
@@ -71,8 +62,6 @@ class Iterator implements IteratorAggregate
/**
* Returns the current element
*
* @return mixed
*/
public function current()
{
@@ -82,8 +71,6 @@ class Iterator implements IteratorAggregate
/**
* Moves the cursor to the previous element
* and returns it
*
* @return mixed
*/
public function prev()
{
@@ -93,8 +80,6 @@ class Iterator implements IteratorAggregate
/**
* Moves the cursor to the next element
* and returns it
*
* @return mixed
*/
public function next()
{
@@ -104,15 +89,13 @@ class Iterator implements IteratorAggregate
/**
* Moves the cursor to the first element
*/
public function rewind()
public function rewind(): void
{
reset($this->data);
}
/**
* Checks if the current element is valid
*
* @return bool
*/
public function valid(): bool
{
@@ -121,8 +104,6 @@ class Iterator implements IteratorAggregate
/**
* Counts all elements
*
* @return int
*/
public function count(): int
{
@@ -135,7 +116,7 @@ class Iterator implements IteratorAggregate
* @param mixed $needle the element to search for
* @return int|false the index (int) of the element or false
*/
public function indexOf($needle)
public function indexOf($needle): int|false
{
return array_search($needle, array_values($this->data));
}
@@ -144,9 +125,9 @@ class Iterator implements IteratorAggregate
* Tries to find the key for the given element
*
* @param mixed $needle the element to search for
* @return string|false the name of the key or false
* @return int|string|false the name of the key or false
*/
public function keyOf($needle)
public function keyOf($needle): int|string|false
{
return array_search($needle, $this->data);
}
@@ -155,18 +136,16 @@ class Iterator implements IteratorAggregate
* Checks by key if an element is included
*
* @param mixed $key
* @return bool
*/
public function has($key): bool
{
return isset($this->data[$key]);
return isset($this->data[$key]) === true;
}
/**
* Checks if the current key is set
*
* @param mixed $key the key to check
* @return bool
*/
public function __isset($key): bool
{
@@ -175,8 +154,6 @@ class Iterator implements IteratorAggregate
/**
* Simplified var_dump output
*
* @return array
*/
public function __debugInfo(): array
{

View File

@@ -28,25 +28,17 @@ class Locale
/**
* Converts a normalized locale array to an array with the
* locale constants replaced with their string representations
*
* @param array $locale
* @return array
*/
public static function export(array $locale): array
{
$return = [];
$constants = static::supportedConstants(true);
// replace the keys in the locale data array with the locale names
$return = [];
foreach ($locale as $key => $value) {
if (isset($constants[$key]) === true) {
// the key is a valid constant,
// replace it with its string representation
$return[$constants[$key]] = $value;
} else {
// not found, keep it as-is
$return[$key] = $value;
}
// use string representation for key
// if it is a valid constant
$return[$constants[$key] ?? $key] = $value;
}
return $return;
@@ -63,7 +55,7 @@ class Locale
* @throws \Kirby\Exception\Exception If the locale cannot be determined
* @throws \Kirby\Exception\InvalidArgumentException If the provided locale category is invalid
*/
public static function get($category = LC_ALL)
public static function get(int|string $category = LC_ALL): array|string
{
$normalizedCategory = static::normalizeConstant($category);
@@ -105,11 +97,10 @@ class Locale
* string keys to a normalized constant => value array
*
* @param array|string $locale
* @return array
*/
public static function normalize($locale): array
{
if (is_array($locale)) {
if (is_array($locale) === true) {
// replace string constant keys with the constant values
$convertedLocale = [];
foreach ($locale as $key => $value) {
@@ -129,11 +120,8 @@ class Locale
/**
* Sets the PHP locale with a locale string or
* an array with constant or string keys
*
* @param array|string $locale
* @return void
*/
public static function set($locale): void
public static function set(array|string $locale): void
{
$locale = static::normalize($locale);
@@ -154,13 +142,13 @@ class Locale
/**
* Tries to convert an `LC_*` constant name
* to its constant value
*
* @param int|string $constant
* @return int|string
*/
protected static function normalizeConstant($constant)
protected static function normalizeConstant(int|string $constant): int|string
{
if (is_string($constant) === true && Str::startsWith($constant, 'LC_') === true) {
if (
is_string($constant) === true &&
Str::startsWith($constant, 'LC_') === true
) {
return constant($constant);
}
@@ -173,7 +161,6 @@ class Locale
* that are actually supported on this system
*
* @param bool $withAll If set to `true`, `LC_ALL` is returned as well
* @return array
*/
protected static function supportedConstants(bool $withAll = false): array
{

View File

@@ -19,8 +19,6 @@ class Obj extends stdClass
{
/**
* Constructor
*
* @param array $data
*/
public function __construct(array $data = [])
{
@@ -31,10 +29,6 @@ class Obj extends stdClass
/**
* Magic getter
*
* @param string $property
* @param array $arguments
* @return mixed
*/
public function __call(string $property, array $arguments)
{
@@ -43,8 +37,6 @@ class Obj extends stdClass
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
@@ -53,9 +45,6 @@ class Obj extends stdClass
/**
* Magic property getter
*
* @param string $property
* @return mixed
*/
public function __get(string $property)
{
@@ -65,19 +54,15 @@ class Obj extends stdClass
/**
* Gets one or multiple properties of the object
*
* @param string|array $property
* @param mixed $fallback If multiple properties are requested:
* Associative array of fallback values per key
* @return mixed
*/
public function get($property, $fallback = null)
public function get(string|array $property, $fallback = null)
{
if (is_array($property)) {
if ($fallback === null) {
$fallback = [];
}
$fallback ??= [];
if (!is_array($fallback)) {
if (is_array($fallback) === false) {
throw new InvalidArgumentException('The fallback value must be an array when getting multiple properties');
}
@@ -93,8 +78,6 @@ class Obj extends stdClass
/**
* Converts the object to an array
*
* @return array
*/
public function toArray(): array
{
@@ -116,9 +99,6 @@ class Obj extends stdClass
/**
* Converts the object to a json string
*
* @param mixed ...$arguments
* @return string
*/
public function toJson(...$arguments): string
{

View File

@@ -389,9 +389,7 @@ class Pagination
// ensure that page is set to something, otherwise
// generate "default page" based on other params
if ($this->page === null) {
$this->page = $this->firstPage();
}
$this->page ??= $this->firstPage();
// allow a page value of 1 even if there are no pages;
// otherwise the exception will get thrown for this pretty common case

View File

@@ -3,6 +3,7 @@
namespace Kirby\Toolkit;
use Closure;
use Kirby\Cms\Helpers;
use Kirby\Exception\BadMethodCallException;
use Kirby\Exception\InvalidArgumentException;
@@ -18,7 +19,6 @@ use Kirby\Exception\InvalidArgumentException;
* @license https://opensource.org/licenses/MIT
*
* @deprecated 3.8.2 Use `Kirby\Query\Query` instead
* // TODO: throw warnings in 3.9.0
* // TODO: Remove in 3.10.0
*/
class Query
@@ -57,6 +57,8 @@ class Query
{
$this->query = $query;
$this->data = $data;
Helpers::deprecated('The `Toolkit\Query` class has been deprecated and will be removed in a future version. Use `Query\Query` instead: Kirby\Query\Query::factory($query)->resolve($data).');
}
/**

View File

@@ -15,19 +15,12 @@ namespace Kirby\Toolkit;
*/
class Silo
{
/**
* @var array
*/
public static $data = [];
/**
* Setter for new data.
*
* @param string|array $key
* @param mixed $value
* @return array
* Setter for new data
*/
public static function set($key, $value = null): array
public static function set(string|array $key, $value = null): array
{
if (is_array($key) === true) {
return static::$data = array_merge(static::$data, $key);
@@ -37,12 +30,7 @@ class Silo
return static::$data;
}
/**
* @param string|array $key
* @param mixed $default
* @return mixed
*/
public static function get($key = null, $default = null)
public static function get(string|array $key = null, $default = null)
{
if ($key === null) {
return static::$data;
@@ -53,9 +41,6 @@ class Silo
/**
* Removes an item from the data array
*
* @param string|null $key
* @return array
*/
public static function remove(string $key = null): array
{

View File

@@ -25,17 +25,13 @@ class Str
{
/**
* Language translation table
*
* @var array
*/
public static $language = [];
public static array $language = [];
/**
* Ascii translation table
*
* @var array
*/
public static $ascii = [
public static array $ascii = [
'/°|₀/' => '0',
'/¹|₁/' => '1',
'/²|₂/' => '2',
@@ -117,10 +113,8 @@ class Str
/**
* Default settings for class methods
*
* @var array
*/
public static $defaults = [
public static array $defaults = [
'slug' => [
'separator' => '-',
'allowed' => 'a-z0-9'
@@ -130,9 +124,6 @@ class Str
/**
* Parse accepted values and their quality from an
* accept string like an Accept or Accept-Language header
*
* @param string $input
* @return array
*/
public static function accepted(string $input): array
{
@@ -174,11 +165,6 @@ class Str
/**
* Returns the rest of the string after the given substring or character
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function after(string $string, string $needle, bool $caseInsensitive = false): string
{
@@ -192,13 +178,9 @@ class Str
}
/**
* Removes the given substring or character only from the start of the string
* Removes the given substring or character
* only from the start of the string
* @since 3.7.0
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function afterStart(string $string, string $needle, bool $caseInsensitive = false): string
{
@@ -215,9 +197,6 @@ class Str
/**
* Convert a string to 7-bit ASCII.
*
* @param string $string
* @return string
*/
public static function ascii(string $string): string
{
@@ -238,11 +217,6 @@ class Str
/**
* Returns the beginning of a string before the given substring or character
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function before(string $string, string $needle, bool $caseInsensitive = false): string
{
@@ -258,11 +232,6 @@ class Str
/**
* Removes the given substring or character only from the end of the string
* @since 3.7.0
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function beforeEnd(string $string, string $needle, bool $caseInsensitive = false): string
{
@@ -279,11 +248,6 @@ class Str
/**
* Returns everything between two strings from the first occurrence of a given string
*
* @param string $string
* @param string $start
* @param string $end
* @return string
*/
public static function between(string $string = null, string $start, string $end): string
{
@@ -294,7 +258,6 @@ class Str
* Converts a string to camel case
*
* @param string $value The string to convert
* @return string
*/
public static function camel(string $value = null): string
{
@@ -303,11 +266,6 @@ class Str
/**
* Checks if a str contains another string
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return bool
*/
public static function contains(string $string = null, string $needle, bool $caseInsensitive = false): bool
{
@@ -323,12 +281,9 @@ class Str
* Convert timestamp to date string
* according to locale settings
*
* @param int|null $time
* @param string|\IntlDateFormatter|null $format
* @param string $handler date, intl or strftime
* @return string|int
*/
public static function date(int|null $time = null, $format = null, string $handler = 'date')
public static function date(int|null $time = null, string|IntlDateFormatter $format = null, string $handler = 'date'): string|int|false
{
if (is_null($format) === true) {
return $time;
@@ -365,18 +320,11 @@ class Str
/**
* Converts a string to a different encoding
*
* @param string $string
* @param string $targetEncoding
* @param string $sourceEncoding (optional)
* @return string
*/
public static function convert($string, $targetEncoding, $sourceEncoding = null)
public static function convert(string $string, string $targetEncoding, string $sourceEncoding = null): string
{
// detect the source encoding if not passed as third argument
if ($sourceEncoding === null) {
$sourceEncoding = static::encoding($string);
}
$sourceEncoding ??= static::encoding($string);
// no need to convert if the target encoding is the same
if (strtolower($sourceEncoding) === strtolower($targetEncoding)) {
@@ -388,9 +336,6 @@ class Str
/**
* Encode a string (used for email addresses)
*
* @param string $string
* @return string
*/
public static function encode(string $string): string
{
@@ -407,22 +352,18 @@ class Str
/**
* Tries to detect the string encoding
*
* @param string $string
* @return string
*/
public static function encoding(string $string): string
{
return mb_detect_encoding($string, 'UTF-8, ISO-8859-1, windows-1251', true);
return mb_detect_encoding(
$string,
'UTF-8, ISO-8859-1, windows-1251',
true
);
}
/**
* Checks if a string ends with the passed needle
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return bool
*/
public static function endsWith(string $string = null, string $needle, bool $caseInsensitive = false): bool
{
@@ -499,11 +440,8 @@ class Str
/**
* Convert the value to a float with a decimal
* point, no matter what the locale setting is
*
* @param string|int|float $value
* @return string
*/
public static function float($value): string
public static function float(string|int|float|null $value): string
{
// make sure $value is not null
$value ??= '';
@@ -520,11 +458,6 @@ class Str
/**
* Returns the rest of the string starting from the given character
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function from(string $string, string $needle, bool $caseInsensitive = false): string
{
@@ -542,9 +475,7 @@ class Str
* @since 3.7.0
*
* @param string $string The string to increment
* @param string $separator
* @param int $first Starting number
* @return string
*/
public static function increment(string $string, string $separator = '-', int $first = 1): string
{
@@ -561,9 +492,6 @@ class Str
/**
* Convert a string to kebab case.
*
* @param string $value
* @return string
*/
public static function kebab(string $value = null): string
{
@@ -572,9 +500,6 @@ class Str
/**
* A UTF-8 safe version of strlen()
*
* @param string $string
* @return int
*/
public static function length(string $string = null): int
{
@@ -583,9 +508,6 @@ class Str
/**
* A UTF-8 safe version of strtolower()
*
* @param string $string
* @return string
*/
public static function lower(string $string = null): string
{
@@ -594,10 +516,6 @@ class Str
/**
* Safe ltrim alternative
*
* @param string $string
* @param string $trim
* @return string
*/
public static function ltrim(string $string, string $trim = ' '): string
{
@@ -607,12 +525,8 @@ class Str
/**
* Get a character pool with various possible combinations
*
* @param string|array $type
* @param bool $array
* @return string|array
*/
public static function pool($type, bool $array = true)
public static function pool(string|array $type, bool $array = true): string|array
{
$pool = [];
@@ -638,12 +552,9 @@ class Str
* Returns the position of a needle in a string
* if it can be found
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return int|bool
* @throws \Kirby\Exception\InvalidArgumentException for empty $needle
*/
public static function position(string $string = null, string $needle, bool $caseInsensitive = false)
public static function position(string $string = null, string $needle, bool $caseInsensitive = false): int|bool
{
if ($needle === '') {
throw new InvalidArgumentException('The needle must not be empty');
@@ -660,10 +571,6 @@ class Str
/**
* Runs a string query.
* Check out the Query class for more information.
*
* @param string $query
* @param array $data
* @return string|null
*/
public static function query(string $query, array $data = [])
{
@@ -675,15 +582,11 @@ class Str
*
* @param int $length The length of the random string
* @param string $type Pool type (type of allowed characters)
* @return string
*/
public static function random(int $length = null, string $type = 'alphaNum')
public static function random(int $length = null, string $type = 'alphaNum'): string|false
{
if ($length === null) {
$length = random_int(5, 10);
}
$pool = static::pool($type, false);
$length ??= random_int(5, 10);
$pool = static::pool($type, false);
// catch invalid pools
if (!$pool) {
@@ -883,10 +786,17 @@ class Str
*
* @return string The filled-in and partially escaped string
*/
public static function safeTemplate(string $string = null, array $data = [], array $options = []): string
{
$callback = ($options['callback'] ?? null) instanceof Closure ? $options['callback'] : null;
public static function safeTemplate(
string $string = null,
array $data = [],
array $options = []
): string {
$fallback = $options['fallback'] ?? null;
$callback = $options['callback'] ?? null;
if ($callback instanceof Closure === false) {
$callback = null;
}
// replace and escape
$string = static::template($string, $data, [
@@ -957,8 +867,7 @@ class Str
* @author Based on the work of Antal Áron
* @copyright Original Copyright (c) 2017, Antal Áron
* @license https://github.com/antalaron/mb-similar-text/blob/master/LICENSE MIT License
* @param string $first
* @param string $second
*
* @param bool $caseInsensitive If `true`, strings are compared case-insensitively
* @return array matches: Number of matching chars in both strings
* percent: Similarity in percent
@@ -1033,8 +942,12 @@ class Str
* @param int $maxlength The maximum length of the slug
* @return string The safe string
*/
public static function slug(string $string = null, string $separator = null, string $allowed = null, int $maxlength = 128): string
{
public static function slug(
string $string = null,
string $separator = null,
string $allowed = null,
int $maxlength = 128
): string {
$separator ??= static::$defaults['slug']['separator'];
$allowed ??= static::$defaults['slug']['allowed'];
@@ -1063,10 +976,6 @@ class Str
/**
* Convert a string to snake case.
*
* @param string $value
* @param string $delimiter
* @return string
*/
public static function snake(string $value = null, string $delimiter = '_'): string
{
@@ -1088,7 +997,7 @@ class Str
* @param int $length The min length of values.
* @return array An array of found values
*/
public static function split($string, string $separator = ',', int $length = 1): array
public static function split(string|array|null $string, string $separator = ',', int $length = 1): array
{
if (is_array($string) === true) {
return $string;
@@ -1112,11 +1021,6 @@ class Str
/**
* Checks if a string starts with the passed needle
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return bool
*/
public static function startsWith(string $string = null, string $needle, bool $caseInsensitive = false): bool
{
@@ -1132,7 +1036,6 @@ class Str
* @since 3.7.0
*
* @param string $value The string to convert
* @return string
*/
public static function studly(string $value = null): string
{
@@ -1141,11 +1044,6 @@ class Str
/**
* A UTF-8 safe version of substr()
*
* @param string $string
* @param int $start
* @param int $length
* @return string
*/
public static function substr(string $string = null, int $start = 0, int $length = null): string
{
@@ -1173,12 +1071,19 @@ class Str
* - end: end placeholder
* @return string The filled-in string
*/
public static function template(string $string = null, array $data = [], array $options = []): string
{
$fallback = $options['fallback'] ?? null;
$callback = ($options['callback'] ?? null) instanceof Closure ? $options['callback'] : null;
public static function template(
string $string = null,
array $data = [],
array $options = []
): string {
$start = (string)($options['start'] ?? '{{');
$end = (string)($options['end'] ?? '}}');
$fallback = $options['fallback'] ?? null;
$callback = $options['callback'] ?? null;
if ($callback instanceof Closure === false) {
$callback = null;
}
// make sure $string is string
$string ??= '';
@@ -1219,9 +1124,6 @@ class Str
/**
* Converts a filesize string with shortcuts
* like M, G or K to an integer value
*
* @param string $size
* @return int
*/
public static function toBytes(string $size): int
{
@@ -1229,26 +1131,18 @@ class Str
$last = strtolower($size[strlen($size)-1] ?? '');
$size = (int)$size;
switch ($last) {
case 'g':
$size *= 1024;
// no break
case 'm':
$size *= 1024;
// no break
case 'k':
$size *= 1024;
}
$size *= match ($last) {
'g' => 1024 * 1024 * 1024,
'm' => 1024 * 1024,
'k' => 1024,
default => 1
};
return $size;
}
/**
* Convert the string to the given type
*
* @param string $string
* @param mixed $type
* @return mixed
*/
public static function toType($string, $type)
{
@@ -1267,10 +1161,6 @@ class Str
/**
* Safe trim alternative
*
* @param string $string
* @param string $trim
* @return string
*/
public static function trim(string $string, string $trim = ' '): string
{
@@ -1279,20 +1169,16 @@ class Str
/**
* A UTF-8 safe version of ucfirst()
*
* @param string $string
* @return string
*/
public static function ucfirst(string $string = null): string
{
return static::upper(static::substr($string, 0, 1)) . static::lower(static::substr($string, 1));
$first = static::substr($string, 0, 1);
$rest = static::substr($string, 1);
return static::upper($first) . static::lower($rest);
}
/**
* A UTF-8 safe version of ucwords()
*
* @param string $string
* @return string
*/
public static function ucwords(string $string = null): string
{
@@ -1308,9 +1194,6 @@ class Str
* // output: some uber crazy stuff
*
* </code>
*
* @param string $string
* @return string The html string
*/
public static function unhtml(string $string = null): string
{
@@ -1319,11 +1202,6 @@ class Str
/**
* Returns the beginning of a string until the given character
*
* @param string $string
* @param string $needle
* @param bool $caseInsensitive
* @return string
*/
public static function until(string $string, string $needle, bool $caseInsensitive = false): string
{
@@ -1338,9 +1216,6 @@ class Str
/**
* A UTF-8 safe version of strotoupper()
*
* @param string $string
* @return string
*/
public static function upper(string $string = null): string
{
@@ -1351,8 +1226,6 @@ class Str
* Creates a compliant v4 UUID
* Taken from: https://github.com/symfony/polyfill
* @since 3.7.0
*
* @return string
*/
public static function uuid(): string
{
@@ -1381,9 +1254,6 @@ class Str
* The widont function makes sure that there are no
* typographical widows at the end of a paragraph
* that's a single word in the last line
*
* @param string $string
* @return string
*/
public static function widont(string $string = null): string
{
@@ -1411,7 +1281,6 @@ class Str
* @param string $string String to wrap
* @param string $before String to prepend
* @param string|null $after String to append (if different from `$before`)
* @return string
*/
public static function wrap(string $string, string $before, string $after = null): string
{

View File

@@ -19,9 +19,6 @@ class Tpl
/**
* Renders the template
*
* @param string|null $file
* @param array $data
* @return string
* @throws Throwable
*/
public static function load(string|null $file = null, array $data = []): string

View File

@@ -24,22 +24,15 @@ class V
{
/**
* An array with all installed validators
*
* @var array
*/
public static $validators = [];
public static array $validators = [];
/**
* Validates the given input with all passed rules
* and returns an array with all error messages.
* The array will be empty if the input is valid
*
* @param mixed $input
* @param array $rules
* @param array $messages
* @return array
*/
public static function errors($input, array $rules, $messages = []): array
public static function errors($input, array $rules, array $messages = []): array
{
$errors = static::value($input, $rules, $messages, false);
@@ -50,11 +43,6 @@ class V
* Runs a number of validators on a set of data and
* checks if the data is invalid
* @since 3.7.0
*
* @param array $data
* @param array $rules
* @param array $messages
* @return array
*/
public static function invalid(array $data = [], array $rules = [], array $messages = []): array
{
@@ -119,10 +107,6 @@ class V
* Creates a useful error message for the given validator
* and the arguments. This is used mainly internally
* to create error messages
*
* @param string $validatorName
* @param mixed ...$params
* @return string|null
*/
public static function message(string $validatorName, ...$params): string|null
{
@@ -162,8 +146,6 @@ class V
/**
* Return the list of all validators
*
* @return array
*/
public static function validators(): array
{
@@ -174,14 +156,8 @@ class V
* Validate a single value against
* a set of rules, using all registered
* validators
*
* @param mixed $value
* @param array $rules
* @param array $messages
* @param bool $fail
* @return bool|array
*/
public static function value($value, array $rules, array $messages = [], bool $fail = true)
public static function value($value, array $rules, array $messages = [], bool $fail = true): bool|array
{
$errors = [];
@@ -214,10 +190,6 @@ class V
* Validate an input array against
* a set of rules, using all registered
* validators
*
* @param array $input
* @param array $rules
* @return bool
*/
public static function input(array $input, array $rules): bool
{
@@ -252,10 +224,6 @@ class V
/**
* Calls an installed validator and passes all arguments
*
* @param string $method
* @param array $arguments
* @return bool
*/
public static function __callStatic(string $method, array $arguments): bool
{
@@ -301,8 +269,9 @@ V::$validators = [
* Checks for numbers within the given range
*/
'between' => function ($value, $min, $max): bool {
return V::min($value, $min) === true &&
V::max($value, $max) === true;
return
V::min($value, $min) === true &&
V::max($value, $max) === true;
},
/**
@@ -415,8 +384,9 @@ V::$validators = [
* Checks for a valid filename
*/
'filename' => function ($value): bool {
return V::match($value, '/^[a-z0-9@._-]+$/i') === true &&
V::min($value, 2) === true;
return
V::match($value, '/^[a-z0-9@._-]+$/i') === true &&
V::min($value, 2) === true;
},
/**

View File

@@ -19,23 +19,16 @@ class View
{
/**
* The absolute path to the view file
*
* @var string
*/
protected $file;
protected string $file;
/**
* The view data
*
* @var array
*/
protected $data = [];
protected array $data = [];
/**
* Creates a new view object
*
* @param string $file
* @param array $data
*/
public function __construct(string $file, array $data = [])
{
@@ -46,8 +39,6 @@ class View
/**
* Returns the view's data array
* without globals.
*
* @return array
*/
public function data(): array
{
@@ -56,8 +47,6 @@ class View
/**
* Checks if the template file exists
*
* @return bool
*/
public function exists(): bool
{
@@ -66,18 +55,14 @@ class View
/**
* Returns the view file
*
* @return string|false
*/
public function file()
public function file(): string
{
return $this->file;
}
/**
* Creates an error message for the missing view exception
*
* @return string
*/
protected function missingViewMessage(): string
{
@@ -86,8 +71,6 @@ class View
/**
* Renders the view
*
* @return string
*/
public function render(): string
{
@@ -115,9 +98,7 @@ class View
}
/**
* Alias for View::render()
*
* @return string
* @see ::render()
*/
public function toString(): string
{
@@ -127,8 +108,6 @@ class View
/**
* Magic string converter to enable
* converting view objects to string
*
* @return string
*/
public function __toString(): string
{

View File

@@ -2,6 +2,7 @@
namespace Kirby\Toolkit;
use Kirby\Cms\Helpers;
use SimpleXMLElement;
/**
@@ -17,10 +18,8 @@ class Xml
{
/**
* HTML to XML conversion table for entities
*
* @var array
*/
public static $entities = [
public static array|null $entities = [
'&nbsp;' => '&#160;', '&iexcl;' => '&#161;', '&cent;' => '&#162;', '&pound;' => '&#163;', '&curren;' => '&#164;', '&yen;' => '&#165;', '&brvbar;' => '&#166;', '&sect;' => '&#167;',
'&uml;' => '&#168;', '&copy;' => '&#169;', '&ordf;' => '&#170;', '&laquo;' => '&#171;', '&not;' => '&#172;', '&shy;' => '&#173;', '&reg;' => '&#174;', '&macr;' => '&#175;',
'&deg;' => '&#176;', '&plusmn;' => '&#177;', '&sup2;' => '&#178;', '&sup3;' => '&#179;', '&acute;' => '&#180;', '&micro;' => '&#181;', '&para;' => '&#182;', '&middot;' => '&#183;',
@@ -71,7 +70,7 @@ class Xml
* If used with a `$name` array, this can be set to `false` to disable attribute sorting.
* @return string|null The generated XML attributes string
*/
public static function attr($name, $value = null): string|null
public static function attr(string|array $name, $value = null): string|null
{
if (is_array($name) === true) {
if ($value !== false) {
@@ -80,26 +79,43 @@ class Xml
$attributes = [];
foreach ($name as $key => $val) {
$a = static::attr($key, $val);
if (is_int($key) === true) {
$key = $val;
$val = true;
}
if ($a) {
$attributes[] = $a;
if ($attribute = static::attr($key, $val)) {
$attributes[] = $attribute;
}
}
return implode(' ', $attributes);
}
// TODO: In 3.10, treat $value === '' to render as name=""
if ($value === null || $value === '' || $value === []) {
// TODO: Remove in 3.10
// @codeCoverageIgnoreStart
if ($value === '') {
Helpers::deprecated('Passing an empty string as value to `Xml::attr()` has been deprecated. In a future version, passing an empty string won\'t omit the attribute anymore but render it with an empty value. To omit the attribute, please pass `null`.');
}
// @codeCoverageIgnoreEnd
return null;
}
// TODO: In 3.10, add deprecation message for space = empty attribute
// TODO: In 3.11, render space as space
if ($value === ' ') {
return strtolower($name) . '=""';
return $name . '=""';
}
if (is_bool($value) === true) {
return $value === true ? strtolower($name) . '="' . strtolower($name) . '"' : null;
if ($value === true) {
return $name . '="' . $name . '"';
}
if ($value === false) {
return null;
}
if (is_array($value) === true) {
@@ -115,7 +131,7 @@ class Xml
$value = static::encode($value);
}
return strtolower($name) . '="' . $value . '"';
return $name . '="' . $value . '"';
}
/**
@@ -133,8 +149,13 @@ class Xml
* @param int $level The indentation level (used internally)
* @return string The XML string
*/
public static function create($props, string $name = 'root', bool $head = true, string $indent = ' ', int $level = 0): string
{
public static function create(
array|string $props,
string $name = 'root',
bool $head = true,
string $indent = ' ',
int $level = 0
): string {
if (is_array($props) === true) {
if (A::isAssociative($props) === true) {
// a tag with attributes or named children
@@ -177,7 +198,7 @@ class Xml
} else {
// scalar value
$result = static::tag($name, $props, null, $indent, $level);
$result = static::tag($name, $props, [], $indent, $level);
}
if ($head === true) {
@@ -194,17 +215,10 @@ class Xml
* echo Xml::decode('some &uuml;ber <em>crazy</em> stuff');
* // output: some über crazy stuff
* ```
*
* @param string|null $string
* @return string
*/
public static function decode(string|null $string): string
{
if ($string === null) {
$string = '';
}
$string = strip_tags($string);
$string = strip_tags($string ?? '');
return html_entity_decode($string, ENT_COMPAT, 'utf-8');
}
@@ -219,9 +233,7 @@ class Xml
* // output: some &#252;ber crazy stuff
* ```
*
* @param string|null $string
* @param bool $html True = Convert to HTML-safe first
* @return string
*/
public static function encode(string|null $string, bool $html = true): string
{
@@ -242,8 +254,6 @@ class Xml
/**
* Returns the HTML-to-XML entity translation table
*
* @return array
*/
public static function entities(): array
{
@@ -253,7 +263,6 @@ class Xml
/**
* Parses an XML string and returns an array
*
* @param string $xml
* @return array|null Parsed array or `null` on error
*/
public static function parse(string $xml): array|null
@@ -271,11 +280,9 @@ class Xml
* Breaks a SimpleXMLElement down into a simpler tree
* structure of arrays and strings
*
* @param \SimpleXMLElement $element
* @param bool $collectName Whether the element name should be collected (for the root element)
* @return array|string
*/
public static function simplify(SimpleXMLElement $element, bool $collectName = true)
public static function simplify(SimpleXMLElement $element, bool $collectName = true): array|string
{
// get all XML namespaces of the whole document to iterate over later;
// we don't need the global namespace (empty string) in the list
@@ -365,7 +372,7 @@ class Xml
* @param int $level Indentation level
* @return string The generated XML
*/
public static function tag(string $name, $content = '', array $attr = null, string|null $indent = null, int $level = 0): string
public static function tag(string $name, $content = '', array $attr = [], string $indent = null, int $level = 0): string
{
$attr = static::attr($attr);
$start = '<' . $name . ($attr ? ' ' . $attr : '') . '>';
@@ -394,9 +401,6 @@ class Xml
/**
* Properly encodes tag contents
*
* @param mixed $value
* @return string|null
*/
public static function value($value): string|null
{