Update Composer packages
This commit is contained in:
parent
9252d9ce90
commit
134266af8a
176 changed files with 7930 additions and 2262 deletions
|
@ -26,7 +26,7 @@ class NodeString extends NodeProperty
|
|||
public static function factory($value = null): static|null
|
||||
{
|
||||
if ($value === null) {
|
||||
return $value;
|
||||
return null;
|
||||
}
|
||||
|
||||
return new static($value);
|
||||
|
|
|
@ -67,10 +67,10 @@ class Api extends BaseApi
|
|||
* Returns the file object for the given
|
||||
* parent path and filename
|
||||
*
|
||||
* @param string|null $path Path to file's parent model
|
||||
* @param string $path Path to file's parent model
|
||||
* @throws \Kirby\Exception\NotFoundException if the file cannot be found
|
||||
*/
|
||||
public function file(string|null $path = null, string $filename): File|null
|
||||
public function file(string $path, string $filename): File|null
|
||||
{
|
||||
return Find::file($path, $filename);
|
||||
}
|
||||
|
|
|
@ -203,7 +203,6 @@ class App
|
|||
/**
|
||||
* Applies a hook to the given value
|
||||
*
|
||||
* @internal
|
||||
* @param string $name Full event name
|
||||
* @param array $args Associative array of named event arguments
|
||||
* @param string $modify Key in $args that is modified by the hooks
|
||||
|
@ -364,25 +363,23 @@ class App
|
|||
* by name. All relevant dependencies are
|
||||
* automatically injected
|
||||
*
|
||||
* @param string $name
|
||||
* @return \Kirby\Cms\Collection|null
|
||||
* @return \Kirby\Toolkit\Collection|null
|
||||
* @todo 5.0 Add return type declaration
|
||||
*/
|
||||
public function collection(string $name)
|
||||
public function collection(string $name, array $options = [])
|
||||
{
|
||||
return $this->collections()->get($name, [
|
||||
return $this->collections()->get($name, array_merge($options, [
|
||||
'kirby' => $this,
|
||||
'site' => $this->site(),
|
||||
'pages' => $this->site()->children(),
|
||||
'site' => $site = $this->site(),
|
||||
'pages' => $site->children(),
|
||||
'users' => $this->users()
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all user-defined collections
|
||||
*
|
||||
* @return \Kirby\Cms\Collections
|
||||
*/
|
||||
public function collections()
|
||||
public function collections(): Collections
|
||||
{
|
||||
return $this->collections ??= new Collections();
|
||||
}
|
||||
|
@ -582,7 +579,14 @@ class App
|
|||
$visitor = $this->visitor();
|
||||
|
||||
foreach ($visitor->acceptedLanguages() as $acceptedLang) {
|
||||
$closure = fn ($language) => $language->locale(LC_ALL) === $acceptedLang->locale();
|
||||
$closure = function ($language) use ($acceptedLang) {
|
||||
$languageLocale = $language->locale(LC_ALL);
|
||||
$acceptedLocale = $acceptedLang->locale();
|
||||
|
||||
return $languageLocale === $acceptedLocale ||
|
||||
$acceptedLocale === Str::substr($languageLocale, 0, 2);
|
||||
};
|
||||
|
||||
if ($language = $languages->filter($closure)?->first()) {
|
||||
return $language;
|
||||
}
|
||||
|
@ -1670,7 +1674,6 @@ class App
|
|||
/**
|
||||
* Trigger a hook by name
|
||||
*
|
||||
* @internal
|
||||
* @param string $name Full event name
|
||||
* @param array $args Associative array of named event arguments
|
||||
* @param \Kirby\Cms\Event|null $originalEvent Event object (internal use)
|
||||
|
|
|
@ -73,7 +73,7 @@ trait AppErrors
|
|||
$handler = null;
|
||||
|
||||
if ($this->option('debug') === true) {
|
||||
if ($this->option('whoops', true) === true) {
|
||||
if ($this->option('whoops', true) !== false) {
|
||||
$handler = new PrettyPageHandler();
|
||||
$handler->setPageTitle('Kirby CMS Debugger');
|
||||
$handler->setResourcesPath(dirname(__DIR__, 2) . '/assets');
|
||||
|
@ -82,6 +82,14 @@ trait AppErrors
|
|||
if ($editor = $this->option('editor')) {
|
||||
$handler->setEditor($editor);
|
||||
}
|
||||
|
||||
if ($blocklist = $this->option('whoops.blocklist')) {
|
||||
foreach ($blocklist as $superglobal => $vars) {
|
||||
foreach ($vars as $var) {
|
||||
$handler->blacklist($superglobal, $var);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$handler = new CallbackHandler(function ($exception, $inspector, $run) {
|
||||
|
|
|
@ -66,7 +66,7 @@ trait AppUsers
|
|||
} finally {
|
||||
// ensure that the impersonation is *always* reset
|
||||
// to the original value, even if an error occurred
|
||||
$auth->impersonate($userBefore !== null ? $userBefore->id() : null);
|
||||
$auth->impersonate($userBefore?->id());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -279,18 +279,39 @@ class Auth
|
|||
|
||||
$id = $session->data()->get('kirby.userId');
|
||||
|
||||
// if no user is logged in, return immediately
|
||||
if (is_string($id) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user = $this->kirby->users()->find($id)) {
|
||||
// in case the session needs to be updated, do it now
|
||||
// for better performance
|
||||
$session->commit();
|
||||
return $user;
|
||||
// a user is logged in, ensure it exists
|
||||
$user = $this->kirby->users()->find($id);
|
||||
if ($user === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
if ($passwordTimestamp = $user->passwordTimestamp()) {
|
||||
$loginTimestamp = $session->data()->get('kirby.loginTimestamp');
|
||||
if (is_int($loginTimestamp) !== true) {
|
||||
// session that was created before Kirby
|
||||
// 3.5.8.3, 3.6.6.3, 3.7.5.2, 3.8.4.1 or 3.9.6
|
||||
// or when the user didn't have a password set
|
||||
$user->logout();
|
||||
return null;
|
||||
}
|
||||
|
||||
// invalidate the session if the password
|
||||
// changed since the login
|
||||
if ($loginTimestamp < $passwordTimestamp) {
|
||||
$user->logout();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// in case the session needs to be updated, do it now
|
||||
// for better performance
|
||||
$session->commit();
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -66,9 +66,11 @@ class Collection extends BaseCollection
|
|||
}
|
||||
|
||||
/**
|
||||
* Internal setter for each object in the Collection.
|
||||
* This takes care of Component validation and of setting
|
||||
* the collection prop on each object correctly.
|
||||
* Internal setter for each object in the Collection;
|
||||
* override from the Toolkit Collection is needed to
|
||||
* make the CMS collections case-sensitive;
|
||||
* child classes can override it again to add validation
|
||||
* and custom behavior depending on the object type
|
||||
*
|
||||
* @param string $id
|
||||
* @param object $object
|
||||
|
@ -79,6 +81,16 @@ class Collection extends BaseCollection
|
|||
$this->data[$id] = $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal remover for each object in the Collection;
|
||||
* override from the Toolkit Collection is needed to
|
||||
* make the CMS collections case-sensitive
|
||||
*/
|
||||
public function __unset($id)
|
||||
{
|
||||
unset($this->data[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single object or
|
||||
* an entire second collection to the
|
||||
|
@ -168,9 +180,7 @@ class Collection extends BaseCollection
|
|||
}
|
||||
|
||||
// ignore upper/lowercase for group names
|
||||
if ($i) {
|
||||
$value = Str::lower($value);
|
||||
}
|
||||
$value = $i === true ? Str::lower($value) : (string)$value;
|
||||
|
||||
if (isset($groups->data[$value]) === false) {
|
||||
// create a new entry for the group if it does not exist yet
|
||||
|
@ -209,9 +219,9 @@ class Collection extends BaseCollection
|
|||
* or ids and then search accordingly.
|
||||
*
|
||||
* @param string|object $needle
|
||||
* @return int
|
||||
* @return int|false
|
||||
*/
|
||||
public function indexOf($needle): int
|
||||
public function indexOf($needle): int|false
|
||||
{
|
||||
if (is_string($needle) === true) {
|
||||
return array_search($needle, $this->keys());
|
||||
|
|
|
@ -28,25 +28,20 @@ class Collections
|
|||
* has been called, to avoid further
|
||||
* processing on sequential calls to
|
||||
* the same collection.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cache = [];
|
||||
protected array $cache = [];
|
||||
|
||||
/**
|
||||
* Store of all collections
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $collections = [];
|
||||
protected array $collections = [];
|
||||
|
||||
/**
|
||||
* Magic caller to enable something like
|
||||
* `$collections->myCollection()`
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return \Kirby\Cms\Collection|null
|
||||
* @return \Kirby\Toolkit\Collection|null
|
||||
* @todo 5.0 Add return type declaration
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
|
@ -56,9 +51,9 @@ class Collections
|
|||
/**
|
||||
* Loads a collection by name if registered
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
* @return \Kirby\Cms\Collection|null
|
||||
* @return \Kirby\Toolkit\Collection|null
|
||||
* @todo 4.0 Add deprecation warning when anything else than a Collection is returned
|
||||
* @todo 5.0 Add return type declaration
|
||||
*/
|
||||
public function get(string $name, array $data = [])
|
||||
{
|
||||
|
|
|
@ -207,7 +207,7 @@ class FileRules
|
|||
if (
|
||||
Str::contains($extension, 'php') !== false ||
|
||||
Str::contains($extension, 'phar') !== false ||
|
||||
Str::contains($extension, 'phtml') !== false
|
||||
Str::contains($extension, 'pht') !== false
|
||||
) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.type.forbidden',
|
||||
|
|
|
@ -23,12 +23,12 @@ class Find
|
|||
* Returns the file object for the given
|
||||
* parent path and filename
|
||||
*
|
||||
* @param string|null $path Path to file's parent model
|
||||
* @param string $path Path to file's parent model
|
||||
* @param string $filename Filename
|
||||
* @return \Kirby\Cms\File|null
|
||||
* @throws \Kirby\Exception\NotFoundException if the file cannot be found
|
||||
*/
|
||||
public static function file(string $path = null, string $filename)
|
||||
public static function file(string $path, string $filename)
|
||||
{
|
||||
$filename = urldecode($filename);
|
||||
$file = static::parent($path)->file($filename);
|
||||
|
@ -121,7 +121,10 @@ class Find
|
|||
'site' => $kirby->site(),
|
||||
'account' => static::user(),
|
||||
'page' => static::page(basename($path)),
|
||||
'file' => static::file(...explode('/files/', $path)),
|
||||
// regular expression to split the path at the last
|
||||
// occurrence of /files/ which separates parent path
|
||||
// and filename
|
||||
'file' => static::file(...preg_split('$.*\K(/files/)$', $path)),
|
||||
'user' => $kirby->user(basename($path)),
|
||||
default => throw new InvalidArgumentException('Invalid model type: ' . $modelType)
|
||||
};
|
||||
|
|
|
@ -95,7 +95,7 @@ trait HasFiles
|
|||
|
||||
// find by global UUID
|
||||
if (Uuid::is($filename, 'file') === true) {
|
||||
return Uuid::for($filename, $this->files())->model();
|
||||
return Uuid::for($filename, $this->$in())->model();
|
||||
}
|
||||
|
||||
if (strpos($filename, '/') !== false) {
|
||||
|
|
|
@ -19,9 +19,9 @@ trait HasSiblings
|
|||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return int
|
||||
* @return int|false
|
||||
*/
|
||||
public function indexOf($collection = null): int
|
||||
public function indexOf($collection = null): int|false
|
||||
{
|
||||
$collection ??= $this->siblingsCollection();
|
||||
return $collection->indexOf($this);
|
||||
|
@ -29,10 +29,13 @@ trait HasSiblings
|
|||
|
||||
/**
|
||||
* Returns the next item in the collection if available
|
||||
* @todo `static` return type hint is not 100% accurate because of
|
||||
* quirks in the `Form` classes; would break if enforced
|
||||
* (https://github.com/getkirby/kirby/pull/5175)
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Model|null
|
||||
* @return static|null
|
||||
*/
|
||||
public function next($collection = null)
|
||||
{
|
||||
|
@ -55,10 +58,13 @@ trait HasSiblings
|
|||
|
||||
/**
|
||||
* Returns the previous item in the collection if available
|
||||
* @todo `static` return type hint is not 100% accurate because of
|
||||
* quirks in the `Form` classes; would break if enforced
|
||||
* (https://github.com/getkirby/kirby/pull/5175)
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Model|null
|
||||
* @return static|null
|
||||
*/
|
||||
public function prev($collection = null)
|
||||
{
|
||||
|
|
|
@ -201,8 +201,17 @@ class Language extends Model
|
|||
*/
|
||||
public static function create(array $props)
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$user = $kirby->user();
|
||||
|
||||
if (
|
||||
$user === null ||
|
||||
$user->role()->permissions()->for('languages', 'create') === false
|
||||
) {
|
||||
throw new PermissionException(['key' => 'language.create.permission']);
|
||||
}
|
||||
|
||||
$props['code'] = Str::slug($props['code'] ?? null);
|
||||
$kirby = App::instance();
|
||||
$languages = $kirby->languages();
|
||||
|
||||
// make the first language the default language
|
||||
|
@ -256,10 +265,18 @@ class Language extends Model
|
|||
public function delete(): bool
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$user = $kirby->user();
|
||||
$languages = $kirby->languages();
|
||||
$code = $this->code();
|
||||
$isLast = $languages->count() === 1;
|
||||
|
||||
if (
|
||||
$user === null ||
|
||||
$user->role()->permissions()->for('languages', 'delete') === false
|
||||
) {
|
||||
throw new PermissionException(['key' => 'language.delete.permission']);
|
||||
}
|
||||
|
||||
// trigger before hook
|
||||
$kirby->trigger('language.delete:before', [
|
||||
'language' => $this
|
||||
|
@ -672,13 +689,22 @@ class Language extends Model
|
|||
*/
|
||||
public function update(array $props = null)
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$user = $kirby->user();
|
||||
|
||||
if (
|
||||
$user === null ||
|
||||
$user->role()->permissions()->for('languages', 'update') === false
|
||||
) {
|
||||
throw new PermissionException(['key' => 'language.update.permission']);
|
||||
}
|
||||
|
||||
// don't change the language code
|
||||
unset($props['code']);
|
||||
|
||||
// make sure the slug is nice and clean
|
||||
$props['slug'] = Str::slug($props['slug'] ?? null);
|
||||
|
||||
$kirby = App::instance();
|
||||
$updated = $this->clone($props);
|
||||
|
||||
// validate the updated language
|
||||
|
|
|
@ -733,7 +733,7 @@ class Page extends ModelWithContent
|
|||
*/
|
||||
public function isListed(): bool
|
||||
{
|
||||
return $this->num() !== null;
|
||||
return $this->isPublished() && $this->num() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -797,7 +797,7 @@ class Page extends ModelWithContent
|
|||
*/
|
||||
public function isUnlisted(): bool
|
||||
{
|
||||
return $this->isListed() === false;
|
||||
return $this->isPublished() && $this->num() === null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -811,7 +811,7 @@ class Page extends ModelWithContent
|
|||
public function isVerified(string $token = null)
|
||||
{
|
||||
if (
|
||||
$this->isDraft() === false &&
|
||||
$this->isPublished() === true &&
|
||||
$this->parents()->findBy('status', 'draft') === null
|
||||
) {
|
||||
return true;
|
||||
|
|
|
@ -270,9 +270,9 @@ class Pages extends Collection
|
|||
$query = $startAt;
|
||||
|
||||
foreach ($path as $key) {
|
||||
$collection = $item ? $item->children() : $this;
|
||||
$query = ltrim($query . '/' . $key, '/');
|
||||
$item = $collection->get($query) ?? null;
|
||||
$collection = $item?->children() ?? $this;
|
||||
$query = ltrim($query . '/' . $key, '/');
|
||||
$item = $collection->get($query) ?? null;
|
||||
|
||||
if ($item === null && $multiLang === true && !App::instance()->language()->isDefault()) {
|
||||
if (count($path) > 1 || $collection->parent()) {
|
||||
|
|
|
@ -44,7 +44,8 @@ class Permissions
|
|||
],
|
||||
'languages' => [
|
||||
'create' => true,
|
||||
'delete' => true
|
||||
'delete' => true,
|
||||
'update' => true
|
||||
],
|
||||
'pages' => [
|
||||
'changeSlug' => true,
|
||||
|
|
|
@ -206,6 +206,20 @@ class UpdateStatus
|
|||
];
|
||||
}
|
||||
|
||||
// add special message for end-of-life PHP versions
|
||||
$phpMajor = PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;
|
||||
$phpEol = $this->data['php'][$phpMajor] ?? null;
|
||||
if (is_string($phpEol) === true && $eolTime = strtotime($phpEol)) {
|
||||
// the timestamp is available and valid, now check if it is in the past
|
||||
if ($eolTime < time()) {
|
||||
$messages[] = [
|
||||
'text' => I18n::template('system.issues.eol.php', null, ['release' => $phpMajor]),
|
||||
'link' => 'https://getkirby.com/security/php-end-of-life',
|
||||
'icon' => 'bell'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->messages = $messages;
|
||||
}
|
||||
|
||||
|
|
|
@ -352,7 +352,7 @@ class User extends ModelWithContent
|
|||
*/
|
||||
public function isKirby(): bool
|
||||
{
|
||||
return $this->email() === 'kirby@getkirby.com';
|
||||
return $this->isAdmin() && $this->id() === 'kirby';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -396,7 +396,7 @@ class User extends ModelWithContent
|
|||
*/
|
||||
public function isNobody(): bool
|
||||
{
|
||||
return $this->email() === 'nobody@getkirby.com';
|
||||
return $this->role()->id() === 'nobody' && $this->id() === 'nobody';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -406,7 +406,9 @@ class User extends ModelWithContent
|
|||
*/
|
||||
public function language(): string
|
||||
{
|
||||
return $this->language ??= $this->credentials()['language'] ?? $this->kirby()->panelLanguage();
|
||||
return $this->language ??=
|
||||
$this->credentials()['language'] ??
|
||||
$this->kirby()->panelLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -441,6 +443,9 @@ class User extends ModelWithContent
|
|||
|
||||
$session->regenerateToken(); // privilege change
|
||||
$session->data()->set('kirby.userId', $this->id());
|
||||
if ($this->passwordTimestamp() !== null) {
|
||||
$session->data()->set('kirby.loginTimestamp', time());
|
||||
}
|
||||
$this->kirby()->auth()->setUser($this);
|
||||
|
||||
$kirby->trigger('user.login:after', ['user' => $this, 'session' => $session]);
|
||||
|
@ -461,6 +466,7 @@ class User extends ModelWithContent
|
|||
|
||||
// remove the user from the session for future requests
|
||||
$session->data()->remove('kirby.userId');
|
||||
$session->data()->remove('kirby.loginTimestamp');
|
||||
|
||||
// clear the cached user object from the app state of the current request
|
||||
$this->kirby()->auth()->flush();
|
||||
|
@ -607,6 +613,26 @@ class User extends ModelWithContent
|
|||
return $this->password = $this->readPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp when the password
|
||||
* was last changed
|
||||
*/
|
||||
public function passwordTimestamp(): int|null
|
||||
{
|
||||
$file = $this->passwordFile();
|
||||
|
||||
// ensure we have the latest information
|
||||
// to prevent cache attacks
|
||||
clearstatcache();
|
||||
|
||||
// user does not have a password
|
||||
if (is_file($file) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return filemtime($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Kirby\Cms\UserPermissions
|
||||
*/
|
||||
|
@ -864,14 +890,29 @@ class User extends ModelWithContent
|
|||
throw new NotFoundException(['key' => 'user.password.undefined']);
|
||||
}
|
||||
|
||||
// `UserRules` enforces a minimum length of 8 characters,
|
||||
// so everything below that is a typo
|
||||
if (Str::length($password) < 8) {
|
||||
throw new InvalidArgumentException(['key' => 'user.password.invalid']);
|
||||
}
|
||||
|
||||
// too long passwords can cause DoS attacks
|
||||
if (Str::length($password) > 1000) {
|
||||
throw new InvalidArgumentException(['key' => 'user.password.excessive']);
|
||||
}
|
||||
|
||||
if (password_verify($password, $this->password()) !== true) {
|
||||
throw new InvalidArgumentException(['key' => 'user.password.wrong', 'httpCode' => 401]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the password file
|
||||
*/
|
||||
protected function passwordFile(): string
|
||||
{
|
||||
return $this->root() . '/.htpasswd';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,13 @@ trait UserActions
|
|||
// update the users collection
|
||||
$user->kirby()->users()->set($user->id(), $user);
|
||||
|
||||
// keep the user logged in to the current browser
|
||||
// if they changed their own password
|
||||
// (regenerate the session token, update the login timestamp)
|
||||
if ($user->isLoggedIn() === true) {
|
||||
$user->loginPasswordless();
|
||||
}
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
@ -323,7 +330,7 @@ trait UserActions
|
|||
*/
|
||||
protected function readPassword()
|
||||
{
|
||||
return F::read($this->root() . '/.htpasswd');
|
||||
return F::read($this->passwordFile());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -384,6 +391,6 @@ trait UserActions
|
|||
#[SensitiveParameter]
|
||||
string $password = null
|
||||
): bool {
|
||||
return F::write($this->root() . '/.htpasswd', $password);
|
||||
return F::write($this->passwordFile(), $password);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -301,8 +301,8 @@ class UserRules
|
|||
*/
|
||||
public static function validId(User $user, string $id): bool
|
||||
{
|
||||
if ($id === 'account') {
|
||||
throw new InvalidArgumentException('"account" is a reserved word and cannot be used as user id');
|
||||
if (in_array($id, ['account', 'kirby', 'nobody']) === true) {
|
||||
throw new InvalidArgumentException('"' . $id . '" is a reserved word and cannot be used as user id');
|
||||
}
|
||||
|
||||
if ($user->kirby()->users()->find($id)) {
|
||||
|
@ -341,12 +341,23 @@ class UserRules
|
|||
#[SensitiveParameter]
|
||||
string $password
|
||||
): bool {
|
||||
// too short passwords are ineffective
|
||||
if (Str::length($password ?? null) < 8) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'user.password.invalid',
|
||||
]);
|
||||
}
|
||||
|
||||
// too long passwords can cause DoS attacks
|
||||
// and are therefore blocked in the auth system
|
||||
// (blocked here as well to avoid passwords
|
||||
// that cannot be used to log in)
|
||||
if (Str::length($password ?? null) > 1000) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'user.password.excessive',
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -93,10 +93,14 @@ class Txt extends Handler
|
|||
throw new InvalidArgumentException('Invalid TXT data; please pass a string');
|
||||
}
|
||||
|
||||
// remove BOM
|
||||
$string = str_replace("\xEF\xBB\xBF", '', $string);
|
||||
// remove Unicode BOM at the beginning of the file
|
||||
if (Str::startsWith($string, "\xEF\xBB\xBF") === true) {
|
||||
$string = substr($string, 3);
|
||||
}
|
||||
|
||||
// explode all fields by the line separator
|
||||
$fields = preg_split('!\n----\s*\n*!', $string);
|
||||
|
||||
// start the data array
|
||||
$data = [];
|
||||
|
||||
|
|
43
kirby/src/Data/YamlSpyc.php
Normal file
43
kirby/src/Data/YamlSpyc.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Spyc;
|
||||
|
||||
/**
|
||||
* Simple Wrapper around the Spyc YAML class
|
||||
*
|
||||
* @package Kirby Data
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class YamlSpyc
|
||||
{
|
||||
/**
|
||||
* Converts an array to an encoded YAML string
|
||||
*/
|
||||
public static function encode($data): string
|
||||
{
|
||||
// $data, $indent, $wordwrap, $no_opening_dashes
|
||||
return Spyc::YAMLDump($data, false, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an encoded YAML string and returns a multi-dimensional array
|
||||
*/
|
||||
public static function decode($string): array
|
||||
{
|
||||
$result = Spyc::YAMLLoadString($string);
|
||||
|
||||
if (is_array($result) === true) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// apparently Spyc always returns an array, even for invalid YAML syntax
|
||||
// so this Exception should currently never be thrown
|
||||
throw new InvalidArgumentException('The YAML data cannot be parsed'); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
44
kirby/src/Data/YamlSymfony.php
Normal file
44
kirby/src/Data/YamlSymfony.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Toolkit\A;
|
||||
use Symfony\Component\Yaml\Yaml as Symfony;
|
||||
|
||||
/**
|
||||
* Simple Wrapper around the Symfony YAML class
|
||||
*
|
||||
* @package Kirby Data
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class YamlSymfony
|
||||
{
|
||||
/**
|
||||
* Converts an array to an encoded YAML string
|
||||
*/
|
||||
public static function encode($data): string
|
||||
{
|
||||
$kirby = App::instance(null, true);
|
||||
|
||||
return Symfony::dump(
|
||||
$data,
|
||||
$kirby?->option('yaml.params.inline') ?? 9999,
|
||||
$kirby?->option('yaml.params.indent') ?? 2,
|
||||
Symfony::DUMP_MULTI_LINE_LITERAL_BLOCK | Symfony::DUMP_EMPTY_ARRAY_AS_SEQUENCE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an encoded YAML string and returns a multi-dimensional array
|
||||
*/
|
||||
public static function decode($string): array
|
||||
{
|
||||
$result = Symfony::parse($string);
|
||||
$result = A::wrap($result);
|
||||
return $result;
|
||||
}
|
||||
}
|
|
@ -3,9 +3,10 @@
|
|||
namespace Kirby\Database;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Collection;
|
||||
use Kirby\Toolkit\Obj;
|
||||
use Kirby\Toolkit\Str;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
|
@ -62,7 +63,7 @@ class Database
|
|||
/**
|
||||
* The last error
|
||||
*/
|
||||
protected Exception|null $lastError = null;
|
||||
protected Throwable|null $lastError = null;
|
||||
|
||||
/**
|
||||
* The last insert id
|
||||
|
@ -280,7 +281,7 @@ class Database
|
|||
/**
|
||||
* Returns the last db error
|
||||
*/
|
||||
public function lastError(): Throwable
|
||||
public function lastError(): Throwable|null
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
|
@ -337,13 +338,16 @@ class Database
|
|||
/**
|
||||
* Executes a sql query, which is expected to return a set of results
|
||||
*/
|
||||
public function query(string $query, array $bindings = [], array $params = [])
|
||||
{
|
||||
public function query(
|
||||
string $query,
|
||||
array $bindings = [],
|
||||
array $params = []
|
||||
) {
|
||||
$defaults = [
|
||||
'flag' => null,
|
||||
'method' => 'fetchAll',
|
||||
'fetch' => 'Kirby\Toolkit\Obj',
|
||||
'iterator' => 'Kirby\Toolkit\Collection',
|
||||
'fetch' => Obj::class,
|
||||
'iterator' => Collection::class,
|
||||
];
|
||||
|
||||
$options = array_merge($defaults, $params);
|
||||
|
@ -359,7 +363,7 @@ class Database
|
|||
) {
|
||||
$flags = PDO::FETCH_ASSOC;
|
||||
} else {
|
||||
$flags = PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE;
|
||||
$flags = PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE;
|
||||
}
|
||||
|
||||
// add optional flags
|
||||
|
@ -368,7 +372,10 @@ class Database
|
|||
}
|
||||
|
||||
// set the fetch mode
|
||||
if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') {
|
||||
if (
|
||||
$options['fetch'] instanceof Closure ||
|
||||
$options['fetch'] === 'array'
|
||||
) {
|
||||
$this->statement->setFetchMode($flags);
|
||||
} else {
|
||||
$this->statement->setFetchMode($flags, $options['fetch']);
|
||||
|
@ -379,8 +386,14 @@ class Database
|
|||
|
||||
// apply the fetch closure to all results if given
|
||||
if ($options['fetch'] instanceof Closure) {
|
||||
foreach ($results as $key => $result) {
|
||||
$results[$key] = $options['fetch']($result, $key);
|
||||
if ($options['method'] === 'fetchAll') {
|
||||
// fetching multiple records
|
||||
foreach ($results as $key => $result) {
|
||||
$results[$key] = $options['fetch']($result, $key);
|
||||
}
|
||||
} elseif ($options['method'] === 'fetch' && $results !== false) {
|
||||
// fetching a single record
|
||||
$results = $options['fetch']($results, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -189,8 +189,12 @@ class Query
|
|||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function fetch(string|Closure $fetch): static
|
||||
public function fetch(string|callable|Closure $fetch): static
|
||||
{
|
||||
if (is_callable($fetch) === true) {
|
||||
$fetch = Closure::fromCallable($fetch);
|
||||
}
|
||||
|
||||
$this->fetch = $fetch;
|
||||
return $this;
|
||||
}
|
||||
|
@ -623,7 +627,7 @@ class Query
|
|||
/**
|
||||
* Selects only one row from a table
|
||||
*/
|
||||
public function first(): object|array|false
|
||||
public function first(): mixed
|
||||
{
|
||||
return $this->query($this->offset(0)->limit(1)->build('select'), [
|
||||
'fetch' => $this->fetch,
|
||||
|
@ -635,7 +639,7 @@ class Query
|
|||
/**
|
||||
* Selects only one row from a table
|
||||
*/
|
||||
public function row(): object|array|false
|
||||
public function row(): mixed
|
||||
{
|
||||
return $this->first();
|
||||
}
|
||||
|
@ -643,7 +647,7 @@ class Query
|
|||
/**
|
||||
* Selects only one row from a table
|
||||
*/
|
||||
public function one(): object|array|false
|
||||
public function one(): mixed
|
||||
{
|
||||
return $this->first();
|
||||
}
|
||||
|
|
|
@ -354,7 +354,7 @@ class File
|
|||
return F::modified(
|
||||
$this->root(),
|
||||
$format,
|
||||
$handler ?? ($kirby ? $kirby->option('date.handler', 'date') : 'date')
|
||||
$handler ?? $kirby?->option('date.handler', 'date') ?? 'date'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,7 @@ class Mime
|
|||
'php' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'php3' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'phps' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'pht' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'phtml' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'png' => 'image/png',
|
||||
'ppt' => ['application/powerpoint', 'application/vnd.ms-powerpoint'],
|
||||
|
|
|
@ -402,7 +402,7 @@ class Field extends Component
|
|||
if ($formFields !== null) {
|
||||
foreach ($this->when as $field => $value) {
|
||||
$field = $formFields->get($field);
|
||||
$inputValue = $field !== null ? $field->value() : '';
|
||||
$inputValue = $field?->value() ?? '';
|
||||
|
||||
// if the input data doesn't match the requested `when` value,
|
||||
// that means that this field is not required and can be saved
|
||||
|
|
|
@ -49,7 +49,7 @@ class LayoutField extends BlocksField
|
|||
$settings = $this->settings();
|
||||
|
||||
return new Form([
|
||||
'fields' => $settings ? $settings->fields() : [],
|
||||
'fields' => $settings?->fields() ?? [],
|
||||
'model' => $this->model,
|
||||
'strict' => true,
|
||||
'values' => $input,
|
||||
|
@ -66,7 +66,7 @@ class LayoutField extends BlocksField
|
|||
$settings = $this->settings();
|
||||
|
||||
return array_merge(parent::props(), [
|
||||
'settings' => $settings !== null ? $settings->toArray() : null,
|
||||
'settings' => $settings?->toArray(),
|
||||
'layouts' => $this->layouts()
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -427,7 +427,7 @@ abstract class FieldClass
|
|||
if ($formFields !== null) {
|
||||
foreach ($this->when as $field => $value) {
|
||||
$field = $formFields->get($field);
|
||||
$inputValue = $field !== null ? $field->value() : '';
|
||||
$inputValue = $field?->value() ?? '';
|
||||
|
||||
// if the input data doesn't match the requested `when` value,
|
||||
// that means that this field is not required and can be saved
|
||||
|
|
|
@ -377,8 +377,8 @@ class Environment
|
|||
$data['https'] = $this->detectHttpsProtocol($fields['proto']);
|
||||
}
|
||||
|
||||
if ($data['port'] === null && $data['https'] === true) {
|
||||
$data['port'] = 443;
|
||||
if ($data['https'] === true) {
|
||||
$data['port'] ??= 443;
|
||||
}
|
||||
|
||||
$data['for'] = $parts['for'] ?? null;
|
||||
|
@ -772,18 +772,28 @@ class Environment
|
|||
|
||||
/**
|
||||
* Loads and returns options from environment-specific
|
||||
* PHP files (by host name and server IP address)
|
||||
* PHP files (by host name and server IP address or CLI)
|
||||
*
|
||||
* @param string $root Root directory to load configs from
|
||||
*/
|
||||
public function options(string $root): array
|
||||
{
|
||||
$configCli = [];
|
||||
$configHost = [];
|
||||
$configAddr = [];
|
||||
|
||||
$host = $this->host();
|
||||
$addr = $this->ip();
|
||||
|
||||
// load the config for the cli
|
||||
if ($this->cli() === true) {
|
||||
$configCli = F::load(
|
||||
file: $root . '/config.cli.php',
|
||||
fallback: [],
|
||||
allowOutput: false
|
||||
);
|
||||
}
|
||||
|
||||
// load the config for the host
|
||||
if (empty($host) === false) {
|
||||
$configHost = F::load(
|
||||
|
@ -802,7 +812,7 @@ class Environment
|
|||
);
|
||||
}
|
||||
|
||||
return array_replace_recursive($configHost, $configAddr);
|
||||
return array_replace_recursive($configCli, $configHost, $configAddr);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -60,6 +60,8 @@ class Remote
|
|||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @throws \Exception when the curl request failed
|
||||
*/
|
||||
public function __construct(string $url, array $options = [])
|
||||
{
|
||||
|
@ -120,6 +122,7 @@ class Remote
|
|||
* Sets up all curl options and sends the request
|
||||
*
|
||||
* @return $this
|
||||
* @throws \Exception when the curl request failed
|
||||
*/
|
||||
public function fetch(): static
|
||||
{
|
||||
|
@ -258,6 +261,8 @@ class Remote
|
|||
|
||||
/**
|
||||
* Static method to send a GET request
|
||||
*
|
||||
* @throws \Exception when the curl request failed
|
||||
*/
|
||||
public static function get(string $url, array $params = []): static
|
||||
{
|
||||
|
@ -339,6 +344,8 @@ class Remote
|
|||
|
||||
/**
|
||||
* Static method to init this class and send a request
|
||||
*
|
||||
* @throws \Exception when the curl request failed
|
||||
*/
|
||||
public static function request(string $url, array $params = []): static
|
||||
{
|
||||
|
|
|
@ -195,7 +195,7 @@ class Request
|
|||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return array_merge($this->body()->toArray(), $this->query()->toArray());
|
||||
return array_replace($this->body()->toArray(), $this->query()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -210,8 +210,8 @@ class Request
|
|||
// the request method can be overwritten with a header
|
||||
$methodOverride = strtoupper(Environment::getGlobally('HTTP_X_HTTP_METHOD_OVERRIDE', ''));
|
||||
|
||||
if ($method === null && in_array($methodOverride, $methods) === true) {
|
||||
$method = $methodOverride;
|
||||
if (in_array($methodOverride, $methods) === true) {
|
||||
$method ??= $methodOverride;
|
||||
}
|
||||
|
||||
// final chain of options to detect the method
|
||||
|
|
|
@ -176,6 +176,18 @@ class Response
|
|||
'type' => F::extensionToMime(F::extension($file))
|
||||
], $props);
|
||||
|
||||
// if we couldn't serve a correct MIME type, force
|
||||
// the browser to display the file as plain text to
|
||||
// harden against attacks from malicious file uploads
|
||||
if ($props['type'] === null) {
|
||||
if (isset($props['headers']) !== true) {
|
||||
$props['headers'] = [];
|
||||
}
|
||||
|
||||
$props['type'] = 'text/plain';
|
||||
$props['headers']['X-Content-Type-Options'] = 'nosniff';
|
||||
}
|
||||
|
||||
return new static($props);
|
||||
}
|
||||
|
||||
|
|
|
@ -69,10 +69,15 @@ class Document
|
|||
'custom' => static::customAsset('panel.css'),
|
||||
],
|
||||
'icons' => static::favicon($url),
|
||||
// loader for plugins' index.dev.mjs files – inlined, so we provide the code instead of the asset URL
|
||||
// loader for plugins' index.dev.mjs files –
|
||||
// inlined, so we provide the code instead of the asset URL
|
||||
'plugin-imports' => $plugins->read('mjs'),
|
||||
'js' => [
|
||||
'vendor' => [
|
||||
'vue' => [
|
||||
'nonce' => $nonce,
|
||||
'src' => $url . '/js/vue.js'
|
||||
],
|
||||
'vendor' => [
|
||||
'nonce' => $nonce,
|
||||
'src' => $url . '/js/vendor.js',
|
||||
'type' => 'module'
|
||||
|
@ -82,17 +87,17 @@ class Document
|
|||
'src' => $url . '/js/plugins.js',
|
||||
'type' => 'module'
|
||||
],
|
||||
'plugins' => [
|
||||
'plugins' => [
|
||||
'nonce' => $nonce,
|
||||
'src' => $plugins->url('js'),
|
||||
'defer' => true
|
||||
],
|
||||
'custom' => [
|
||||
'custom' => [
|
||||
'nonce' => $nonce,
|
||||
'src' => static::customAsset('panel.js'),
|
||||
'type' => 'module'
|
||||
],
|
||||
'index' => [
|
||||
'index' => [
|
||||
'nonce' => $nonce,
|
||||
'src' => $url . '/js/index.js',
|
||||
'type' => 'module'
|
||||
|
@ -115,6 +120,9 @@ class Document
|
|||
'type' => 'module'
|
||||
];
|
||||
|
||||
// load the development version of Vue
|
||||
$assets['js']['vue']['src'] = $url . '/node_modules/vue/dist/vue.js';
|
||||
|
||||
unset($assets['css']['index'], $assets['js']['vendor']);
|
||||
}
|
||||
|
||||
|
@ -270,6 +278,16 @@ class Document
|
|||
'panelUrl' => $uri->path()->toString(true) . '/',
|
||||
]);
|
||||
|
||||
return new Response($body, 'text/html', $code);
|
||||
$frameAncestors = $kirby->option('panel.frameAncestors');
|
||||
$frameAncestors = match (true) {
|
||||
$frameAncestors === true => "'self'",
|
||||
is_array($frameAncestors) => "'self' " . implode(' ', $frameAncestors),
|
||||
is_string($frameAncestors) => $frameAncestors,
|
||||
default => "'none'"
|
||||
};
|
||||
|
||||
return new Response($body, 'text/html', $code, [
|
||||
'Content-Security-Policy' => 'frame-ancestors ' . $frameAncestors
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use Kirby\Exception\NotFoundException;
|
|||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Http\Router;
|
||||
use Kirby\Http\Uri;
|
||||
use Kirby\Http\Url;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\Tpl;
|
||||
|
@ -538,15 +539,22 @@ class Panel
|
|||
*/
|
||||
public static function url(string|null $url = null): string
|
||||
{
|
||||
$slug = App::instance()->option('panel.slug', 'panel');
|
||||
|
||||
// only touch relative paths
|
||||
if (Url::isAbsolute($url) === false) {
|
||||
$path = trim($url, '/');
|
||||
$kirby = App::instance();
|
||||
$slug = $kirby->option('panel.slug', 'panel');
|
||||
$path = trim($url, '/');
|
||||
|
||||
$baseUri = new Uri($kirby->url());
|
||||
$basePath = trim($baseUri->path()->toString(), '/');
|
||||
|
||||
// removes base path if relative path contains it
|
||||
if (empty($basePath) === false && Str::startsWith($path, $basePath) === true) {
|
||||
$path = Str::after($path, $basePath);
|
||||
}
|
||||
// add the panel slug prefix if it it's not
|
||||
// included in the path yet
|
||||
if (Str::startsWith($path, $slug . '/') === false) {
|
||||
elseif (Str::startsWith($path, $slug . '/') === false) {
|
||||
$path = $slug . '/' . $path;
|
||||
}
|
||||
|
||||
|
|
|
@ -137,7 +137,7 @@ class View
|
|||
$user = $kirby->user();
|
||||
|
||||
// user permissions
|
||||
$permissions = $user ? $user->role()->permissions()->toArray() : [];
|
||||
$permissions = $user?->role()->permissions()->toArray() ?? [];
|
||||
|
||||
// current content language
|
||||
$language = $kirby->language();
|
||||
|
|
|
@ -28,7 +28,15 @@ class Inline
|
|||
public function __construct(DOMNode $node, array $marks = [])
|
||||
{
|
||||
$this->createMarkRules($marks);
|
||||
$this->html = trim(static::parseNode($node, $this->marks) ?? '');
|
||||
|
||||
$html = static::parseNode($node, $this->marks) ?? '';
|
||||
|
||||
// only trim HTML if it doesn't consist of only spaces
|
||||
if (trim($html) !== '') {
|
||||
$html = trim($html);
|
||||
}
|
||||
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -70,6 +70,10 @@ class Argument
|
|||
|
||||
// numeric
|
||||
if (is_numeric($argument) === true) {
|
||||
if (strpos($argument, '.') === false) {
|
||||
return new static((int)$argument);
|
||||
}
|
||||
|
||||
return new static((float)$argument);
|
||||
}
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ Query::$entries['file'] = function (string $id): File|null {
|
|||
};
|
||||
|
||||
Query::$entries['page'] = function (string $id): Page|null {
|
||||
return App::instance()->site()->find($id);
|
||||
return App::instance()->page($id);
|
||||
};
|
||||
|
||||
Query::$entries['site'] = function (): Site {
|
||||
|
|
|
@ -77,19 +77,17 @@ class Segment
|
|||
/**
|
||||
* Automatically resolves the segment depending on the
|
||||
* segment position and the type of the base
|
||||
*
|
||||
* @param mixed $base Current value of the query chain
|
||||
*/
|
||||
public function resolve(mixed $base = null, array|object $data = []): mixed
|
||||
{
|
||||
// resolve arguments to array
|
||||
$args = $this->arguments?->resolve($data) ?? [];
|
||||
|
||||
// 1st segment, start from $data array
|
||||
// 1st segment, use $data as base
|
||||
if ($this->position === 0) {
|
||||
if (is_array($data) == true) {
|
||||
return $this->resolveArray($data, $args);
|
||||
}
|
||||
|
||||
return $this->resolveObject($data, $args);
|
||||
$base = $data;
|
||||
}
|
||||
|
||||
if (is_array($base) === true) {
|
||||
|
@ -109,26 +107,55 @@ class Segment
|
|||
*/
|
||||
protected function resolveArray(array $array, array $args): mixed
|
||||
{
|
||||
if (array_key_exists($this->method, $array) === false) {
|
||||
static::error($array, $this->method, 'property');
|
||||
// the directly provided array takes precedence
|
||||
// to look up a matching entry
|
||||
if (array_key_exists($this->method, $array) === true) {
|
||||
$value = $array[$this->method];
|
||||
|
||||
// if this is a Closure we can directly use it, as
|
||||
// Closures from the $array should always have priority
|
||||
// over the Query::$entries Closures
|
||||
if ($value instanceof Closure) {
|
||||
return $value(...$args);
|
||||
}
|
||||
|
||||
// if we have no arguments to pass, we also can directly
|
||||
// use the value from the $array as it must not be different
|
||||
// to the one from Query::$entries with the same name
|
||||
if ($args === []) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
$value = $array[$this->method];
|
||||
|
||||
if ($value instanceof Closure) {
|
||||
return $value(...$args);
|
||||
// fallback time: only if we are handling the first segment,
|
||||
// we can also try to resolve the segment with an entry from the
|
||||
// default Query::$entries
|
||||
if ($this->position === 0) {
|
||||
if (array_key_exists($this->method, Query::$entries) === true) {
|
||||
return Query::$entries[$this->method](...$args);
|
||||
}
|
||||
}
|
||||
|
||||
if ($args !== []) {
|
||||
// if we have not been able to return anything so far,
|
||||
// we just need to differntiate between two different error messages
|
||||
|
||||
// this one is in case the original array contained the key,
|
||||
// but was not a Closure while the segment had arguments
|
||||
if (
|
||||
array_key_exists($this->method, $array) &&
|
||||
$args !== []
|
||||
) {
|
||||
throw new InvalidArgumentException('Cannot access array element "' . $this->method . '" with arguments');
|
||||
}
|
||||
|
||||
return $value;
|
||||
// last, the standard error for trying to access something
|
||||
// that does not exist
|
||||
static::error($array, $this->method, 'property');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves segment by calling the method/accessing the property
|
||||
* on the base object
|
||||
* Resolves segment by calling the method/
|
||||
* accessing the property on the base object
|
||||
*/
|
||||
protected function resolveObject(object $object, array $args): mixed
|
||||
{
|
||||
|
@ -140,7 +167,8 @@ class Segment
|
|||
}
|
||||
|
||||
if (
|
||||
$args === [] && (
|
||||
$args === [] &&
|
||||
(
|
||||
property_exists($object, $this->method) === true ||
|
||||
method_exists($object, '__get') === true
|
||||
)
|
||||
|
|
|
@ -10,6 +10,7 @@ use Kirby\Exception\NotFoundException;
|
|||
use Kirby\Http\Cookie;
|
||||
use Kirby\Http\Url;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\SymmetricCrypto;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
|
@ -38,7 +39,7 @@ class Session
|
|||
protected $lastActivity;
|
||||
protected $renewable;
|
||||
protected $data;
|
||||
protected $newSession;
|
||||
protected array|null $newSession;
|
||||
|
||||
// temporary state flags
|
||||
protected $updatingLastActivity = false;
|
||||
|
@ -348,15 +349,27 @@ class Session
|
|||
}
|
||||
|
||||
// collect all data
|
||||
if ($this->newSession) {
|
||||
if (isset($this->newSession) === true) {
|
||||
// the token has changed
|
||||
// we are writing to the old session: it only gets the reference to the new session
|
||||
// and a shortened expiry time (30 second grace period)
|
||||
$data = [
|
||||
'startTime' => $this->startTime(),
|
||||
'expiryTime' => time() + 30,
|
||||
'newSession' => $this->newSession
|
||||
'newSession' => $this->newSession[0]
|
||||
];
|
||||
|
||||
// include the token key for the new session if we
|
||||
// have access to the PHP `sodium` extension;
|
||||
// otherwise (if no encryption is possible), the token key
|
||||
// is omitted, which makes the new session read-only
|
||||
// when accessed through the old session
|
||||
if ($crypto = $this->crypto()) {
|
||||
// encrypt the new token key with the old token key
|
||||
// so that attackers with read access to the session file
|
||||
// (e.g. via directory traversal) cannot impersonate the new session
|
||||
$data['newSessionKey'] = $crypto->encrypt($this->newSession[1]);
|
||||
}
|
||||
} else {
|
||||
$data = [
|
||||
'startTime' => $this->startTime(),
|
||||
|
@ -446,7 +459,7 @@ class Session
|
|||
|
||||
// mark the old session as moved if there is one
|
||||
if ($this->tokenExpiry !== null) {
|
||||
$this->newSession = $tokenExpiry . '.' . $tokenId;
|
||||
$this->newSession = [$tokenExpiry . '.' . $tokenId, $tokenKey];
|
||||
$this->commit();
|
||||
|
||||
// we are now in the context of the new session
|
||||
|
@ -536,7 +549,7 @@ class Session
|
|||
}
|
||||
|
||||
// don't allow writing for read-only sessions
|
||||
// (only the case for moved sessions)
|
||||
// (only the case for moved sessions when the PHP `sodium` extension is not available)
|
||||
/**
|
||||
* @todo This check gets flagged by Psalm for unknown reasons
|
||||
* @psalm-suppress ParadoxicalCondition
|
||||
|
@ -555,6 +568,22 @@ class Session
|
|||
$this->writeMode = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a symmetric crypto instance based on the
|
||||
* token key of the session
|
||||
*/
|
||||
protected function crypto(): SymmetricCrypto|null
|
||||
{
|
||||
if (
|
||||
$this->tokenKey === null ||
|
||||
SymmetricCrypto::isAvailable() === false
|
||||
) {
|
||||
return null; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return new SymmetricCrypto(secretKey: hex2bin($this->tokenKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a token string into its parts and sets them as instance vars
|
||||
*
|
||||
|
@ -698,6 +727,20 @@ class Session
|
|||
|
||||
// follow to the new session if there is one
|
||||
if (isset($data['newSession'])) {
|
||||
// decrypt the token key if provided and we have access to
|
||||
// the PHP `sodium` extension for decryption
|
||||
if (
|
||||
isset($data['newSessionKey']) === true &&
|
||||
$crypto = $this->crypto()
|
||||
) {
|
||||
$tokenKey = $crypto->decrypt($data['newSessionKey']);
|
||||
|
||||
$this->parseToken($data['newSession'] . '.' . $tokenKey);
|
||||
$this->init();
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise initialize without the token key (read-only mode)
|
||||
$this->parseToken($data['newSession'], true);
|
||||
$this->init();
|
||||
return;
|
||||
|
|
|
@ -59,6 +59,77 @@ class A
|
|||
return count($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if every element in the array passes the test
|
||||
*
|
||||
* <code>
|
||||
* $array = [1, 30, 39, 29, 10, 13];
|
||||
*
|
||||
* $isBelowThreshold = fn($value) => $value < 40;
|
||||
* echo A::every($array, $isBelowThreshold) ? 'true' : 'false';
|
||||
* // output: 'true'
|
||||
*
|
||||
* $isIntegerKey = fn($value, $key) => is_int($key);
|
||||
* echo A::every($array, $isIntegerKey) ? 'true' : 'false';
|
||||
* // output: 'true'
|
||||
* </code>
|
||||
*
|
||||
* @since 3.9.8
|
||||
* @param array $array
|
||||
* @param callable(mixed $value, int|string $key, array $array):bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public static function every(array $array, callable $test): bool
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
if (!$test($value, $key, $array)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first element matching the given callback
|
||||
*
|
||||
* <code>
|
||||
* $array = [1, 30, 39, 29, 10, 13];
|
||||
*
|
||||
* $isAboveThreshold = fn($value) => $value > 30;
|
||||
* echo A::find($array, $isAboveThreshold);
|
||||
* // output: '39'
|
||||
*
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'cow' => 'moo',
|
||||
* 'colibri' => 'humm',
|
||||
* 'dog' => 'wuff',
|
||||
* 'chicken' => 'cluck',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $keyNotStartingWithC = fn($value, $key) => $key[0] !== 'c';
|
||||
* echo A::find($array, $keyNotStartingWithC);
|
||||
* // output: 'wuff'
|
||||
* </code>
|
||||
*
|
||||
* @since 3.9.8
|
||||
* @param array $array
|
||||
* @param callable(mixed $value, int|string $key, array $array):bool $callback
|
||||
* @return mixed
|
||||
*/
|
||||
public static function find(array $array, callable $callback): mixed
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
if ($callback($value, $key, $array)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an element of an array by key
|
||||
*
|
||||
|
@ -117,20 +188,23 @@ class A
|
|||
$keys = explode('.', $key);
|
||||
$firstKey = array_shift($keys);
|
||||
|
||||
// if the input array also uses dot notation, try to find a subset of the $keys
|
||||
// if the input array also uses dot notation,
|
||||
// try to find a subset of the $keys
|
||||
if (isset($array[$firstKey]) === false) {
|
||||
$currentKey = $firstKey;
|
||||
|
||||
while ($innerKey = array_shift($keys)) {
|
||||
$currentKey .= '.' . $innerKey;
|
||||
|
||||
// the element needs to exist and also needs to be an array; otherwise
|
||||
// we cannot find the remaining keys within it (invalid array structure)
|
||||
// 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
|
||||
) {
|
||||
// $keys only holds the remaining keys that have not been shifted off yet
|
||||
// $keys only holds the remaining keys
|
||||
// that have not been shifted off yet
|
||||
return static::get(
|
||||
$array[$currentKey],
|
||||
implode('.', $keys),
|
||||
|
@ -204,7 +278,10 @@ class A
|
|||
*/
|
||||
public static function keyBy(array $array, string|callable $keyBy): array
|
||||
{
|
||||
$keys = is_callable($keyBy) ? static::map($array, $keyBy) : static::pluck($array, $keyBy);
|
||||
$keys =
|
||||
is_callable($keyBy) ?
|
||||
static::map($array, $keyBy) :
|
||||
static::pluck($array, $keyBy);
|
||||
|
||||
if (count($keys) !== count($array)) {
|
||||
throw new InvalidArgumentException('The "key by" argument must be a valid key or a callable');
|
||||
|
@ -401,6 +478,37 @@ class A
|
|||
return array_slice($array, $offset, $length, $preserveKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if at least one element in the array passes the test
|
||||
*
|
||||
* <code>
|
||||
* $array = [1, 30, 39, 29, 10, 'foo' => 12, 13];
|
||||
*
|
||||
* $isAboveThreshold = fn($value) => $value > 30;
|
||||
* echo A::some($array, $isAboveThreshold) ? 'true' : 'false';
|
||||
* // output: 'true'
|
||||
*
|
||||
* $isStringKey = fn($value, $key) => is_string($key);
|
||||
* echo A::some($array, $isStringKey) ? 'true' : 'false';
|
||||
* // output: 'true'
|
||||
* </code>
|
||||
*
|
||||
* @since 3.9.8
|
||||
* @param array $array
|
||||
* @param callable(mixed $value, int|string $key, array $array):bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public static function some(array $array, callable $test): bool
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
if ($test($value, $key, $array)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums an array
|
||||
*
|
||||
|
@ -600,7 +708,7 @@ class A
|
|||
$key = array_shift($subKeys);
|
||||
|
||||
// skip the magic for ignored keys
|
||||
if (isset($ignore[$key]) === true && $ignore[$key] === true) {
|
||||
if (($ignore[$key] ?? null) === true) {
|
||||
$result[$fullKey] = $value;
|
||||
continue;
|
||||
}
|
||||
|
@ -618,8 +726,7 @@ class A
|
|||
// merge arrays with previous results if necessary
|
||||
// (needed when the same keys are used both with and without dot notation)
|
||||
if (
|
||||
isset($result[$key]) === true &&
|
||||
is_array($result[$key]) === true &&
|
||||
is_array($result[$key] ?? null) === true &&
|
||||
is_array($value) === true
|
||||
) {
|
||||
$value = array_replace_recursive($result[$key], $value);
|
||||
|
@ -704,8 +811,12 @@ class A
|
|||
* PHP by sort flags
|
||||
* @return array The sorted array
|
||||
*/
|
||||
public static function sort(array $array, string $field, string $direction = 'desc', $method = SORT_REGULAR): array
|
||||
{
|
||||
public static function sort(
|
||||
array $array,
|
||||
string $field,
|
||||
string $direction = 'desc',
|
||||
$method = SORT_REGULAR
|
||||
): array {
|
||||
$direction = strtolower($direction) === 'desc' ? SORT_DESC : SORT_ASC;
|
||||
$helper = [];
|
||||
$result = [];
|
||||
|
@ -818,7 +929,7 @@ class A
|
|||
{
|
||||
foreach ($update as $key => $value) {
|
||||
if ($value instanceof Closure) {
|
||||
$value = call_user_func($value, static::get($array, $key));
|
||||
$value = $value(static::get($array, $key));
|
||||
}
|
||||
|
||||
$array[$key] = $value;
|
||||
|
|
|
@ -123,6 +123,10 @@ class Collection extends Iterator implements Countable
|
|||
*/
|
||||
public function __unset($key)
|
||||
{
|
||||
if ($this->caseSensitive !== true) {
|
||||
$key = strtolower($key);
|
||||
}
|
||||
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
|
||||
|
@ -531,7 +535,7 @@ class Collection extends Iterator implements Countable
|
|||
$value = $this->getAttribute($item, $field);
|
||||
|
||||
// ignore upper/lowercase for group names
|
||||
return $i === true ? Str::lower($value) : $value;
|
||||
return $i === true ? Str::lower($value) : (string)$value;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -84,10 +84,6 @@ class Component
|
|||
|
||||
/**
|
||||
* Magic caller for defined methods and properties
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
|
@ -108,9 +104,6 @@ class Component
|
|||
|
||||
/**
|
||||
* Creates a new component for the given type
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $attrs
|
||||
*/
|
||||
public function __construct(string $type, array $attrs = [])
|
||||
{
|
||||
|
@ -142,8 +135,6 @@ class Component
|
|||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
|
@ -153,9 +144,6 @@ class Component
|
|||
/**
|
||||
* Fallback for missing properties to return
|
||||
* null instead of an error
|
||||
*
|
||||
* @param string $attr
|
||||
* @return null
|
||||
*/
|
||||
public function __get(string $attr)
|
||||
{
|
||||
|
@ -167,8 +155,6 @@ class Component
|
|||
* This can be overwritten by extended classes
|
||||
* to define basic options that should always
|
||||
* be applied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function defaults(): array
|
||||
{
|
||||
|
@ -178,9 +164,6 @@ class Component
|
|||
/**
|
||||
* Register all defined props and apply the
|
||||
* passed values.
|
||||
*
|
||||
* @param array $props
|
||||
* @return void
|
||||
*/
|
||||
protected function applyProps(array $props): void
|
||||
{
|
||||
|
@ -208,9 +191,6 @@ class Component
|
|||
/**
|
||||
* Register all computed properties and calculate their values.
|
||||
* This must happen after all props are registered.
|
||||
*
|
||||
* @param array $computed
|
||||
* @return void
|
||||
*/
|
||||
protected function applyComputed(array $computed): void
|
||||
{
|
||||
|
@ -223,9 +203,6 @@ class Component
|
|||
|
||||
/**
|
||||
* Load a component definition by type
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function load(string $type): array
|
||||
{
|
||||
|
@ -248,9 +225,6 @@ class Component
|
|||
* mixes in the defaults from the defaults method and
|
||||
* then injects all additional mixins, defined in the
|
||||
* component options.
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function setup(string $type): array
|
||||
{
|
||||
|
@ -292,8 +266,6 @@ class Component
|
|||
|
||||
/**
|
||||
* Converts all props and computed props to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
|
|
|
@ -26,11 +26,25 @@ class Controller
|
|||
public function arguments(array $data = []): array
|
||||
{
|
||||
$info = new ReflectionFunction($this->function);
|
||||
$args = [];
|
||||
|
||||
return A::map(
|
||||
$info->getParameters(),
|
||||
fn ($parameter) => $data[$parameter->getName()] ?? null
|
||||
);
|
||||
foreach ($info->getParameters() as $param) {
|
||||
$name = $param->getName();
|
||||
|
||||
if ($param->isVariadic() === true) {
|
||||
// variadic ... argument collects all remaining values
|
||||
$args += $data;
|
||||
} elseif (isset($data[$name]) === true) {
|
||||
// use provided argument value if available
|
||||
$args[$name] = $data[$name];
|
||||
} elseif ($param->isDefaultValueAvailable() === false) {
|
||||
// use null for any other arguments that don't define
|
||||
// a default value for themselves
|
||||
$args[$name] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
public function call($bind = null, $data = [])
|
||||
|
@ -44,7 +58,7 @@ class Controller
|
|||
return $this->function->call($bind, ...$args);
|
||||
}
|
||||
|
||||
public static function load(string $file)
|
||||
public static function load(string $file): static|null
|
||||
{
|
||||
if (is_file($file) === false) {
|
||||
return null;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
|
@ -27,8 +28,10 @@ class Date extends DateTime
|
|||
* @param string|int|\DateTimeInterface $datetime Datetime string, UNIX timestamp or object
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
*/
|
||||
public function __construct($datetime = 'now', ?DateTimeZone $timezone = null)
|
||||
{
|
||||
public function __construct(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
) {
|
||||
if (is_int($datetime) === true) {
|
||||
$datetime = date('r', $datetime);
|
||||
}
|
||||
|
@ -42,8 +45,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Returns the datetime in `YYYY-MM-DD hh:mm:ss` format with timezone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
|
@ -58,7 +59,7 @@ class Date extends DateTime
|
|||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the unit name is invalid
|
||||
*/
|
||||
public function ceil(string $unit)
|
||||
public function ceil(string $unit): static
|
||||
{
|
||||
static::validateUnit($unit);
|
||||
|
||||
|
@ -70,20 +71,17 @@ class Date extends DateTime
|
|||
/**
|
||||
* Returns the interval between the provided and the object's datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return \DateInterval
|
||||
*/
|
||||
public function compare($datetime = 'now', ?DateTimeZone $timezone = null)
|
||||
{
|
||||
public function compare(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): DateInterval {
|
||||
return $this->diff(new static($datetime, $timezone));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the day value
|
||||
*
|
||||
* @param int|null $day
|
||||
* @return int
|
||||
*/
|
||||
public function day(int|null $day = null): int
|
||||
{
|
||||
|
@ -103,7 +101,7 @@ class Date extends DateTime
|
|||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the unit name is invalid
|
||||
*/
|
||||
public function floor(string $unit)
|
||||
public function floor(string $unit): static
|
||||
{
|
||||
static::validateUnit($unit);
|
||||
|
||||
|
@ -123,9 +121,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Gets or sets the hour value
|
||||
*
|
||||
* @param int|null $hour
|
||||
* @return int
|
||||
*/
|
||||
public function hour(int|null $hour = null): int
|
||||
{
|
||||
|
@ -140,79 +135,75 @@ class Date extends DateTime
|
|||
/**
|
||||
* Checks if the object's datetime is the same as the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function is($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
public function is(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
return $this == new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is after the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function isAfter($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
public function isAfter(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
return $this > new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is before the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function isBefore($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
public function isBefore(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
return $this < new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is between the given datetimes
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $min
|
||||
* @param string|int|\DateTimeInterface $max
|
||||
* @return bool
|
||||
*/
|
||||
public function isBetween($min, $max): bool
|
||||
{
|
||||
public function isBetween(
|
||||
string|int|DateTimeInterface $min,
|
||||
string|int|DateTimeInterface $max
|
||||
): bool {
|
||||
return $this->isMin($min) === true && $this->isMax($max) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is at or before the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function isMax($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
public function isMax(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
return $this <= new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is at or after the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function isMin($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
public function isMin(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
return $this >= new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the microsecond value
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function microsecond(): int
|
||||
{
|
||||
|
@ -221,8 +212,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Gets the millisecond value
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function millisecond(): int
|
||||
{
|
||||
|
@ -231,9 +220,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Gets or sets the minute value
|
||||
*
|
||||
* @param int|null $minute
|
||||
* @return int
|
||||
*/
|
||||
public function minute(int|null $minute = null): int
|
||||
{
|
||||
|
@ -247,9 +233,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Gets or sets the month value
|
||||
*
|
||||
* @param int|null $month
|
||||
* @return int
|
||||
*/
|
||||
public function month(int|null $month = null): int
|
||||
{
|
||||
|
@ -265,10 +248,10 @@ class Date extends DateTime
|
|||
* Returns the datetime which is nearest to the object's datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface ...$datetime Datetime strings, UNIX timestamps or objects
|
||||
* @return string|int|\DateTimeInterface
|
||||
*/
|
||||
public function nearest(...$datetime)
|
||||
{
|
||||
public function nearest(
|
||||
string|int|DateTimeInterface ...$datetime
|
||||
): string|int|DateTimeInterface {
|
||||
$timestamp = $this->timestamp();
|
||||
$minDiff = PHP_INT_MAX;
|
||||
$nearest = null;
|
||||
|
@ -291,9 +274,8 @@ class Date extends DateTime
|
|||
* Returns an instance of the current datetime
|
||||
*
|
||||
* @param \DateTimeZone|null $timezone
|
||||
* @return static
|
||||
*/
|
||||
public static function now(?DateTimeZone $timezone = null)
|
||||
public static function now(DateTimeZone|null $timezone = null): static
|
||||
{
|
||||
return new static('now', $timezone);
|
||||
}
|
||||
|
@ -301,13 +283,11 @@ class Date extends DateTime
|
|||
/**
|
||||
* Tries to create an instance from the given string
|
||||
* or fails silently by returning `null` on error
|
||||
*
|
||||
* @param string|null $datetime
|
||||
* @param \DateTimeZone|null $timezone
|
||||
* @return static|null
|
||||
*/
|
||||
public static function optional(string|null $datetime = null, ?DateTimeZone $timezone = null)
|
||||
{
|
||||
public static function optional(
|
||||
string|null $datetime = null,
|
||||
DateTimeZone|null $timezone = null
|
||||
): static|null {
|
||||
if (empty($datetime) === true) {
|
||||
return null;
|
||||
}
|
||||
|
@ -328,7 +308,7 @@ class Date extends DateTime
|
|||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the unit name or size is invalid
|
||||
*/
|
||||
public function round(string $unit, int $size = 1)
|
||||
public function round(string $unit, int $size = 1): static
|
||||
{
|
||||
static::validateUnit($unit);
|
||||
|
||||
|
@ -365,12 +345,12 @@ class Date extends DateTime
|
|||
* by the defined step
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param string|null $date
|
||||
* @param int|array|null $step array of `unit` and `size` to round to nearest
|
||||
* @return int|null
|
||||
*/
|
||||
public static function roundedTimestamp(string|null $date = null, $step = null): int|null
|
||||
{
|
||||
public static function roundedTimestamp(
|
||||
string|null $date = null,
|
||||
int|array|null $step = null
|
||||
): int|null {
|
||||
if ($date = static::optional($date)) {
|
||||
if ($step !== null) {
|
||||
$step = static::stepConfig($step, [
|
||||
|
@ -388,9 +368,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Gets or sets the second value
|
||||
*
|
||||
* @param int|null $second
|
||||
* @return int
|
||||
*/
|
||||
public function second(int|null $second = null): int
|
||||
{
|
||||
|
@ -408,8 +385,10 @@ class Date extends DateTime
|
|||
* @param string|int|\DateTimeInterface $datetime Datetime string, UNIX timestamp or object
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
*/
|
||||
public function set($datetime, ?DateTimeZone $timezone = null)
|
||||
{
|
||||
public function set(
|
||||
string|int|DateTimeInterface $datetime,
|
||||
DateTimeZone|null $timezone = null
|
||||
): void {
|
||||
$datetime = new static($datetime, $timezone);
|
||||
$this->setTimestamp($datetime->timestamp());
|
||||
}
|
||||
|
@ -422,13 +401,11 @@ class Date extends DateTime
|
|||
* @param array|null $default Default values to use if one or both values are not provided
|
||||
* @return array
|
||||
*/
|
||||
public static function stepConfig($input = null, array|null $default = null): array
|
||||
{
|
||||
$default ??= [
|
||||
'size' => 1,
|
||||
'unit' => 'day'
|
||||
];
|
||||
|
||||
public static function stepConfig(
|
||||
// no type hint to use InvalidArgumentException at the end
|
||||
$input = null,
|
||||
array|null $default = ['size' => 1, 'unit' => 'day']
|
||||
): array {
|
||||
if ($input === null) {
|
||||
return $default;
|
||||
}
|
||||
|
@ -452,8 +429,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Returns the time in `hh:mm:ss` format
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function time(): string
|
||||
{
|
||||
|
@ -462,8 +437,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Returns the UNIX timestamp
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function timestamp(): int
|
||||
{
|
||||
|
@ -472,21 +445,16 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Returns the timezone object
|
||||
*
|
||||
* @return \DateTimeZone
|
||||
*/
|
||||
public function timezone()
|
||||
public function timezone(): DateTimeZone|false
|
||||
{
|
||||
return $this->getTimezone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of the beginning of the current day
|
||||
*
|
||||
* @param \DateTimeZone|null $timezone
|
||||
* @return static
|
||||
*/
|
||||
public static function today(?DateTimeZone $timezone = null)
|
||||
public static function today(DateTimeZone|null $timezone = null): static
|
||||
{
|
||||
return new static('today', $timezone);
|
||||
}
|
||||
|
@ -497,12 +465,13 @@ class Date extends DateTime
|
|||
*
|
||||
* @param string $mode `date`, `time` or `datetime`
|
||||
* @param bool $timezone Whether the timezone is printed as well
|
||||
* @return string
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the mode is invalid
|
||||
*/
|
||||
public function toString(string $mode = 'datetime', bool $timezone = true): string
|
||||
{
|
||||
public function toString(
|
||||
string $mode = 'datetime',
|
||||
bool $timezone = true
|
||||
): string {
|
||||
$format = match ($mode) {
|
||||
'date' => 'Y-m-d',
|
||||
'time' => 'H:i:s',
|
||||
|
@ -519,9 +488,6 @@ class Date extends DateTime
|
|||
|
||||
/**
|
||||
* Gets or sets the year value
|
||||
*
|
||||
* @param int|null $year
|
||||
* @return int
|
||||
*/
|
||||
public function year(int|null $year = null): int
|
||||
{
|
||||
|
@ -536,9 +502,6 @@ class Date extends DateTime
|
|||
/**
|
||||
* Ensures that the provided string is a valid unit name
|
||||
*
|
||||
* @param string $unit
|
||||
* @return void
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException
|
||||
*/
|
||||
protected static function validateUnit(string $unit): void
|
||||
|
|
|
@ -17,17 +17,11 @@ abstract class Facade
|
|||
/**
|
||||
* Returns the instance that should be
|
||||
* available statically
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public static function instance();
|
||||
|
||||
/**
|
||||
* Proxy for all public instance calls
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public static function __callStatic(string $method, array $args = null)
|
||||
{
|
||||
|
|
|
@ -90,10 +90,11 @@ class Html extends Xml
|
|||
*
|
||||
* @param string $tag Tag name
|
||||
* @param array $arguments Further arguments for the Html::tag() method
|
||||
* @return string
|
||||
*/
|
||||
public static function __callStatic(string $tag, array $arguments = []): string
|
||||
{
|
||||
public static function __callStatic(
|
||||
string $tag,
|
||||
array $arguments = []
|
||||
): string {
|
||||
if (static::isVoid($tag) === true) {
|
||||
return static::tag($tag, null, ...$arguments);
|
||||
}
|
||||
|
@ -133,8 +134,12 @@ class Html extends Xml
|
|||
* @param string|null $after An optional string that will be appended if the result is not empty
|
||||
* @return string|null The generated HTML attributes string
|
||||
*/
|
||||
public static function attr($name, $value = null, string|null $before = null, string|null $after = null): string|null
|
||||
{
|
||||
public static function attr(
|
||||
string|array $name,
|
||||
$value = null,
|
||||
string|null $before = null,
|
||||
string|null $after = null
|
||||
): string|null {
|
||||
// HTML supports boolean attributes without values
|
||||
if (is_array($name) === false && is_bool($value) === true) {
|
||||
return $value === true ? strtolower($name) : null;
|
||||
|
@ -167,9 +172,6 @@ class Html extends Xml
|
|||
|
||||
/**
|
||||
* Converts lines in a string into HTML breaks
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function breaks(string $string): string
|
||||
{
|
||||
|
@ -184,8 +186,11 @@ class Html extends Xml
|
|||
* @param array $attr Additional attributes for the tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function email(string $email, $text = null, array $attr = []): string
|
||||
{
|
||||
public static function email(
|
||||
string $email,
|
||||
string|array|null $text = null,
|
||||
array $attr = []
|
||||
): string {
|
||||
if (empty($email) === true) {
|
||||
return '';
|
||||
}
|
||||
|
@ -214,14 +219,15 @@ class Html extends Xml
|
|||
/**
|
||||
* Converts a string to an HTML-safe string
|
||||
*
|
||||
* @param string|null $string
|
||||
* @param bool $keepTags If true, existing tags won't be escaped
|
||||
* @return string The HTML string
|
||||
*
|
||||
* @psalm-suppress ParamNameMismatch
|
||||
*/
|
||||
public static function encode(string|null $string, bool $keepTags = false): string
|
||||
{
|
||||
public static function encode(
|
||||
string|null $string,
|
||||
bool $keepTags = false
|
||||
): string {
|
||||
if ($string === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -241,8 +247,6 @@ class Html extends Xml
|
|||
|
||||
/**
|
||||
* Returns the entity translation table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function entities(): array
|
||||
{
|
||||
|
@ -257,8 +261,11 @@ class Html extends Xml
|
|||
* @param array $attr Additional attributes for the `<figure>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function figure($content, $caption = '', array $attr = []): string
|
||||
{
|
||||
public static function figure(
|
||||
string|array $content,
|
||||
string|array|null $caption = '',
|
||||
array $attr = []
|
||||
): string {
|
||||
if ($caption) {
|
||||
$figcaption = static::tag('figcaption', $caption);
|
||||
|
||||
|
@ -280,8 +287,11 @@ class Html extends Xml
|
|||
* @param array $attr Additional attributes for the `<script>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function gist(string $url, string|null $file = null, array $attr = []): string
|
||||
{
|
||||
public static function gist(
|
||||
string $url,
|
||||
string|null $file = null,
|
||||
array $attr = []
|
||||
): string {
|
||||
$src = $url . '.js';
|
||||
|
||||
if ($file !== null) {
|
||||
|
@ -294,7 +304,6 @@ class Html extends Xml
|
|||
/**
|
||||
* Creates an `<iframe>`
|
||||
*
|
||||
* @param string $src
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
|
@ -322,9 +331,6 @@ class Html extends Xml
|
|||
|
||||
/**
|
||||
* Checks if a tag is self-closing
|
||||
*
|
||||
* @param string $tag
|
||||
* @return bool
|
||||
*/
|
||||
public static function isVoid(string $tag): bool
|
||||
{
|
||||
|
@ -339,8 +345,11 @@ class Html extends Xml
|
|||
* @param array $attr Additional attributes for the tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function link(string $href, $text = null, array $attr = []): string
|
||||
{
|
||||
public static function link(
|
||||
string $href,
|
||||
string|array|null $text = null,
|
||||
array $attr = []
|
||||
): string {
|
||||
$attr = array_merge(['href' => $href], $attr);
|
||||
|
||||
if (empty($text) === true) {
|
||||
|
@ -364,8 +373,10 @@ class Html extends Xml
|
|||
* @param string|null $target Current `target` value
|
||||
* @return string|null New `rel` value or `null` if not needed
|
||||
*/
|
||||
public static function rel(string|null $rel = null, string|null $target = null): string|null
|
||||
{
|
||||
public static function rel(
|
||||
string|null $rel = null,
|
||||
string|null $target = null
|
||||
): string|null {
|
||||
$rel = trim($rel ?? '');
|
||||
|
||||
if ($target === '_blank') {
|
||||
|
@ -390,8 +401,13 @@ class Html extends Xml
|
|||
* @param int $level Indentation level
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function tag(string $name, $content = '', array $attr = [], string $indent = null, int $level = 0): string
|
||||
{
|
||||
public static function tag(
|
||||
string $name,
|
||||
array|string|null $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
|
||||
$content ??= '';
|
||||
|
@ -412,8 +428,11 @@ class Html extends Xml
|
|||
* @param array $attr Additional attributes for the tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function tel(string $tel, $text = null, array $attr = []): string
|
||||
{
|
||||
public static function tel(
|
||||
string $tel,
|
||||
string|array|null $text = null,
|
||||
array $attr = []
|
||||
): string {
|
||||
$number = preg_replace('![^0-9\+]+!', '', $tel);
|
||||
|
||||
if (empty($text) === true) {
|
||||
|
@ -425,9 +444,6 @@ class Html extends Xml
|
|||
|
||||
/**
|
||||
* Properly encodes tag contents
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string|null
|
||||
*/
|
||||
public static function value($value): string|null
|
||||
{
|
||||
|
@ -461,8 +477,11 @@ class Html extends Xml
|
|||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string|null The generated HTML
|
||||
*/
|
||||
public static function video(string $url, array $options = [], array $attr = []): string|null
|
||||
{
|
||||
public static function video(
|
||||
string $url,
|
||||
array $options = [],
|
||||
array $attr = []
|
||||
): string|null {
|
||||
// YouTube video
|
||||
if (Str::contains($url, 'youtu', true) === true) {
|
||||
return static::youtube($url, $options['youtube'] ?? [], $attr);
|
||||
|
@ -494,9 +513,6 @@ class Html extends Xml
|
|||
/**
|
||||
* Generates a list of attributes
|
||||
* for video iframes
|
||||
*
|
||||
* @param array $attr
|
||||
* @return array
|
||||
*/
|
||||
public static function videoAttr(array $attr = []): array
|
||||
{
|
||||
|
@ -521,8 +537,11 @@ class Html extends Xml
|
|||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string|null The generated HTML
|
||||
*/
|
||||
public static function vimeo(string $url, array $options = [], array $attr = []): string|null
|
||||
{
|
||||
public static function vimeo(
|
||||
string $url,
|
||||
array $options = [],
|
||||
array $attr = []
|
||||
): string|null {
|
||||
$uri = new Uri($url);
|
||||
$path = $uri->path();
|
||||
$query = $uri->query();
|
||||
|
@ -556,8 +575,11 @@ class Html extends Xml
|
|||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string|null The generated HTML
|
||||
*/
|
||||
public static function youtube(string $url, array $options = [], array $attr = []): string|null
|
||||
{
|
||||
public static function youtube(
|
||||
string $url,
|
||||
array $options = [],
|
||||
array $attr = []
|
||||
): string|null {
|
||||
if (preg_match('!youtu!i', $url) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -23,14 +23,8 @@ use IteratorAggregate;
|
|||
*/
|
||||
class Iterator implements IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* The data array
|
||||
*/
|
||||
public array $data = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
$this->data = $data;
|
||||
|
|
|
@ -17,9 +17,6 @@ use stdClass;
|
|||
*/
|
||||
class Obj extends stdClass
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
foreach ($data as $key => $val) {
|
||||
|
|
|
@ -22,38 +22,28 @@ class Pagination
|
|||
|
||||
/**
|
||||
* The current page
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $page;
|
||||
protected int $page = 1;
|
||||
|
||||
/**
|
||||
* Total number of items
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $total = 0;
|
||||
protected int $total = 0;
|
||||
|
||||
/**
|
||||
* The number of items per page
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $limit = 20;
|
||||
protected int $limit = 20;
|
||||
|
||||
/**
|
||||
* Whether validation of the pagination page
|
||||
* is enabled; will throw Exceptions if true
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $validate = true;
|
||||
public static bool $validate = true;
|
||||
|
||||
/**
|
||||
* Creates a new pagination object
|
||||
* with the given parameters
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props = [])
|
||||
{
|
||||
|
@ -63,53 +53,35 @@ class Pagination
|
|||
/**
|
||||
* Creates a pagination instance for the given
|
||||
* collection with a flexible argument api
|
||||
*
|
||||
* @param \Kirby\Toolkit\Collection $collection
|
||||
* @param mixed ...$arguments
|
||||
* @return static
|
||||
*/
|
||||
public static function for(Collection $collection, ...$arguments)
|
||||
public static function for(Collection $collection, ...$arguments): static
|
||||
{
|
||||
$a = $arguments[0] ?? null;
|
||||
$b = $arguments[1] ?? null;
|
||||
|
||||
$params = [];
|
||||
|
||||
// First argument is a pagination object
|
||||
if ($a instanceof static) {
|
||||
/**
|
||||
* First argument is a pagination/self object
|
||||
*/
|
||||
return $a;
|
||||
} elseif (is_array($a) === true) {
|
||||
/**
|
||||
* First argument is an option array
|
||||
*
|
||||
* $collection->paginate([...])
|
||||
*/
|
||||
}
|
||||
|
||||
if (is_array($a) === true) {
|
||||
// First argument is an option array
|
||||
// $collection->paginate([...])
|
||||
$params = $a;
|
||||
} elseif (is_int($a) === true && $b === null) {
|
||||
/**
|
||||
* First argument is the limit
|
||||
*
|
||||
* $collection->paginate(10)
|
||||
*/
|
||||
// First argument is the limit
|
||||
// $collection->paginate(10)
|
||||
$params['limit'] = $a;
|
||||
} elseif (is_int($a) === true && is_int($b) === true) {
|
||||
/**
|
||||
* First argument is the limit,
|
||||
* second argument is the page
|
||||
*
|
||||
* $collection->paginate(10, 2)
|
||||
*/
|
||||
// First argument is the limit, second argument is the page
|
||||
// $collection->paginate(10, 2)
|
||||
$params['limit'] = $a;
|
||||
$params['page'] = $b;
|
||||
} elseif (is_int($a) === true && is_array($b) === true) {
|
||||
/**
|
||||
* First argument is the limit,
|
||||
* second argument are options
|
||||
*
|
||||
* $collection->paginate(10, [...])
|
||||
*/
|
||||
// First argument is the limit, second argument are options
|
||||
// $collection->paginate(10, [...])
|
||||
$params = $b;
|
||||
$params['limit'] = $a;
|
||||
}
|
||||
|
@ -126,8 +98,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Getter for the current page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function page(): int
|
||||
{
|
||||
|
@ -136,8 +106,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Getter for the total number of items
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function total(): int
|
||||
{
|
||||
|
@ -146,8 +114,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Getter for the number of items per page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function limit(): int
|
||||
{
|
||||
|
@ -156,40 +122,24 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns the index of the first item on the page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function start(): int
|
||||
{
|
||||
$index = $this->page() - 1;
|
||||
|
||||
if ($index < 0) {
|
||||
$index = 0;
|
||||
}
|
||||
|
||||
$index = max(0, $this->page() - 1);
|
||||
return $index * $this->limit() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the last item on the page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function end(): int
|
||||
{
|
||||
$value = ($this->start() - 1) + $this->limit();
|
||||
|
||||
if ($value <= $this->total()) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $this->total();
|
||||
$value = min($this->total(), ($this->start() - 1) + $this->limit());
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of pages
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function pages(): int
|
||||
{
|
||||
|
@ -202,8 +152,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns the first page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function firstPage(): int
|
||||
{
|
||||
|
@ -212,8 +160,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns the last page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function lastPage(): int
|
||||
{
|
||||
|
@ -222,8 +168,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns the offset (i.e. for db queries)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function offset(): int
|
||||
{
|
||||
|
@ -232,9 +176,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Checks if the given page exists
|
||||
*
|
||||
* @param int $page
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPage(int $page): bool
|
||||
{
|
||||
|
@ -251,8 +192,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Checks if there are any pages at all
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPages(): bool
|
||||
{
|
||||
|
@ -261,8 +200,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Checks if there's a previous page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrevPage(): bool
|
||||
{
|
||||
|
@ -271,18 +208,14 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns the previous page
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function prevPage()
|
||||
public function prevPage(): int|null
|
||||
{
|
||||
return $this->hasPrevPage() ? $this->page() - 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a next page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextPage(): bool
|
||||
{
|
||||
|
@ -291,18 +224,14 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns the next page
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function nextPage()
|
||||
public function nextPage(): int|null
|
||||
{
|
||||
return $this->hasNextPage() ? $this->page() + 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current page is the first page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFirstPage(): bool
|
||||
{
|
||||
|
@ -311,8 +240,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Checks if the current page is the last page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLastPage(): bool
|
||||
{
|
||||
|
@ -321,9 +248,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Creates a range of page numbers for Google-like pagination
|
||||
*
|
||||
* @param int $range
|
||||
* @return array
|
||||
*/
|
||||
public function range(int $range = 5): array
|
||||
{
|
||||
|
@ -355,9 +279,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns the first page of the created range
|
||||
*
|
||||
* @param int $range
|
||||
* @return int
|
||||
*/
|
||||
public function rangeStart(int $range = 5): int
|
||||
{
|
||||
|
@ -366,9 +287,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns the last page of the created range
|
||||
*
|
||||
* @param int $range
|
||||
* @return int
|
||||
*/
|
||||
public function rangeEnd(int $range = 5): int
|
||||
{
|
||||
|
@ -383,7 +301,7 @@ class Pagination
|
|||
* @param array $props Array with keys limit, total and/or page
|
||||
* @return $this
|
||||
*/
|
||||
protected function setProperties(array $props)
|
||||
protected function setProperties(array $props): static
|
||||
{
|
||||
$this->baseSetProperties($props);
|
||||
|
||||
|
@ -415,10 +333,9 @@ class Pagination
|
|||
/**
|
||||
* Sets the number of items per page
|
||||
*
|
||||
* @param int $limit
|
||||
* @return $this
|
||||
*/
|
||||
protected function setLimit(int $limit = 20)
|
||||
protected function setLimit(int $limit = 20): static
|
||||
{
|
||||
if ($limit < 1) {
|
||||
throw new Exception('Invalid pagination limit: ' . $limit);
|
||||
|
@ -431,10 +348,9 @@ class Pagination
|
|||
/**
|
||||
* Sets the total number of items
|
||||
*
|
||||
* @param int $total
|
||||
* @return $this
|
||||
*/
|
||||
protected function setTotal(int $total = 0)
|
||||
protected function setTotal(int $total = 0): static
|
||||
{
|
||||
if ($total < 0) {
|
||||
throw new Exception('Invalid total number of items: ' . $total);
|
||||
|
@ -451,7 +367,7 @@ class Pagination
|
|||
* automatically determined if null
|
||||
* @return $this
|
||||
*/
|
||||
protected function setPage($page = null)
|
||||
protected function setPage(int|string|null $page = null): static
|
||||
{
|
||||
// if $page is null, it is set to a default in the setProperties() method
|
||||
if ($page !== null) {
|
||||
|
@ -467,8 +383,6 @@ class Pagination
|
|||
|
||||
/**
|
||||
* Returns an array with all properties
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
|
|
|
@ -166,8 +166,11 @@ class Str
|
|||
/**
|
||||
* Returns the rest of the string after the given substring or character
|
||||
*/
|
||||
public static function after(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
public static function after(
|
||||
string $string,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): string {
|
||||
$position = static::position($string, $needle, $caseInsensitive);
|
||||
|
||||
if ($position === false) {
|
||||
|
@ -182,8 +185,11 @@ class Str
|
|||
* only from the start of the string
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public static function afterStart(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
public static function afterStart(
|
||||
string $string,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): string {
|
||||
if ($needle === '') {
|
||||
return $string;
|
||||
}
|
||||
|
@ -218,8 +224,11 @@ class Str
|
|||
/**
|
||||
* Returns the beginning of a string before the given substring or character
|
||||
*/
|
||||
public static function before(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
public static function before(
|
||||
string $string,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): string {
|
||||
$position = static::position($string, $needle, $caseInsensitive);
|
||||
|
||||
if ($position === false) {
|
||||
|
@ -233,8 +242,11 @@ class Str
|
|||
* Removes the given substring or character only from the end of the string
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public static function beforeEnd(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
public static function beforeEnd(
|
||||
string $string,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): string {
|
||||
if ($needle === '') {
|
||||
return $string;
|
||||
}
|
||||
|
@ -249,8 +261,11 @@ class Str
|
|||
/**
|
||||
* Returns everything between two strings from the first occurrence of a given string
|
||||
*/
|
||||
public static function between(string $string = null, string $start, string $end): string
|
||||
{
|
||||
public static function between(
|
||||
string $string = null,
|
||||
string $start,
|
||||
string $end
|
||||
): string {
|
||||
return static::before(static::after($string, $start), $end);
|
||||
}
|
||||
|
||||
|
@ -267,8 +282,11 @@ class Str
|
|||
/**
|
||||
* Checks if a str contains another string
|
||||
*/
|
||||
public static function contains(string $string = null, string $needle, bool $caseInsensitive = false): bool
|
||||
{
|
||||
public static function contains(
|
||||
string $string = null,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): bool {
|
||||
if ($needle === '') {
|
||||
return true;
|
||||
}
|
||||
|
@ -283,8 +301,11 @@ class Str
|
|||
*
|
||||
* @param string $handler date, intl or strftime
|
||||
*/
|
||||
public static function date(int|null $time = null, string|IntlDateFormatter $format = null, string $handler = 'date'): string|int|false
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
@ -321,8 +342,11 @@ class Str
|
|||
/**
|
||||
* Converts a string to a different encoding
|
||||
*/
|
||||
public static function convert(string $string, string $targetEncoding, string $sourceEncoding = null): string
|
||||
{
|
||||
public static function convert(
|
||||
string $string,
|
||||
string $targetEncoding,
|
||||
string $sourceEncoding = null
|
||||
): string {
|
||||
// detect the source encoding if not passed as third argument
|
||||
$sourceEncoding ??= static::encoding($string);
|
||||
|
||||
|
@ -365,8 +389,11 @@ class Str
|
|||
/**
|
||||
* Checks if a string ends with the passed needle
|
||||
*/
|
||||
public static function endsWith(string $string = null, string $needle, bool $caseInsensitive = false): bool
|
||||
{
|
||||
public static function endsWith(
|
||||
string $string = null,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): bool {
|
||||
if ($needle === '') {
|
||||
return true;
|
||||
}
|
||||
|
@ -389,8 +416,10 @@ class Str
|
|||
* @param string $context Location of output (`html`, `attr`, `js`, `css`, `url` or `xml`)
|
||||
* @return string Escaped data
|
||||
*/
|
||||
public static function esc(string $string, string $context = 'html'): string
|
||||
{
|
||||
public static function esc(
|
||||
string $string,
|
||||
string $context = 'html'
|
||||
): string {
|
||||
if (method_exists(Escape::class, $context) === true) {
|
||||
return Escape::$context($string);
|
||||
}
|
||||
|
@ -409,8 +438,12 @@ class Str
|
|||
* @param string $rep The element, which should be added if the string is too long. Ellipsis is the default.
|
||||
* @return string The shortened string
|
||||
*/
|
||||
public static function excerpt($string, $chars = 140, $strip = true, $rep = ' …')
|
||||
{
|
||||
public static function excerpt(
|
||||
string $string,
|
||||
int $chars = 140,
|
||||
bool $strip = true,
|
||||
string $rep = ' …'
|
||||
): string {
|
||||
if ($strip === true) {
|
||||
// ensure that opening tags are preceded by a space, so that
|
||||
// when tags are skipped we can be sure that words stay separate
|
||||
|
@ -441,8 +474,9 @@ class Str
|
|||
* Convert the value to a float with a decimal
|
||||
* point, no matter what the locale setting is
|
||||
*/
|
||||
public static function float(string|int|float|null $value): string
|
||||
{
|
||||
public static function float(
|
||||
string|int|float|null $value = null
|
||||
): string {
|
||||
// make sure $value is not null
|
||||
$value ??= '';
|
||||
|
||||
|
@ -459,8 +493,11 @@ class Str
|
|||
/**
|
||||
* Returns the rest of the string starting from the given character
|
||||
*/
|
||||
public static function from(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
public static function from(
|
||||
string $string,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): string {
|
||||
$position = static::position($string, $needle, $caseInsensitive);
|
||||
|
||||
if ($position === false) {
|
||||
|
@ -477,8 +514,11 @@ class Str
|
|||
* @param string $string The string to increment
|
||||
* @param int $first Starting number
|
||||
*/
|
||||
public static function increment(string $string, string $separator = '-', int $first = 1): string
|
||||
{
|
||||
public static function increment(
|
||||
string $string,
|
||||
string $separator = '-',
|
||||
int $first = 1
|
||||
): string {
|
||||
preg_match('/(.+)' . preg_quote($separator, '/') . '([0-9]+)$/', $string, $matches);
|
||||
|
||||
if (isset($matches[2]) === true) {
|
||||
|
@ -569,8 +609,10 @@ class Str
|
|||
/**
|
||||
* Get a character pool with various possible combinations
|
||||
*/
|
||||
public static function pool(string|array $type, bool $array = true): string|array
|
||||
{
|
||||
public static function pool(
|
||||
string|array $type,
|
||||
bool $array = true
|
||||
): string|array {
|
||||
$pool = [];
|
||||
|
||||
if (is_array($type) === true) {
|
||||
|
@ -584,6 +626,8 @@ class Str
|
|||
'alpha' => static::pool(['alphaLower', 'alphaUpper']),
|
||||
'num' => range(0, 9),
|
||||
'alphanum' => static::pool(['alpha', 'num']),
|
||||
'base32' => array_merge(static::pool('alphaUpper'), range(2, 7)),
|
||||
'base32hex' => array_merge(range(0, 9), range('A', 'V')),
|
||||
default => $pool
|
||||
};
|
||||
}
|
||||
|
@ -597,8 +641,11 @@ class Str
|
|||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException for empty $needle
|
||||
*/
|
||||
public static function position(string $string = null, string $needle, bool $caseInsensitive = false): int|bool
|
||||
{
|
||||
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');
|
||||
}
|
||||
|
@ -626,8 +673,10 @@ class Str
|
|||
* @param int $length The length of the random string
|
||||
* @param string $type Pool type (type of allowed characters)
|
||||
*/
|
||||
public static function random(int $length = null, string $type = 'alphaNum'): string|false
|
||||
{
|
||||
public static function random(
|
||||
int $length = null,
|
||||
string $type = 'alphaNum'
|
||||
): string|false {
|
||||
$length ??= random_int(5, 10);
|
||||
$pool = static::pool($type, false);
|
||||
|
||||
|
@ -665,9 +714,15 @@ class Str
|
|||
* @return string|array String with replaced values;
|
||||
* if $string is an array, array of strings
|
||||
* @psalm-return ($string is array ? array : string)
|
||||
*
|
||||
* @todo the types aren't correct, refactor to apply native type hinting
|
||||
*/
|
||||
public static function replace($string, $search, $replace, $limit = -1)
|
||||
{
|
||||
public static function replace(
|
||||
$string,
|
||||
$search,
|
||||
$replace,
|
||||
$limit = -1
|
||||
): string|array {
|
||||
// convert Kirby collections to arrays
|
||||
if ($string instanceof Collection) {
|
||||
$string = $string->toArray();
|
||||
|
@ -720,9 +775,14 @@ class Str
|
|||
* defaults to no limit
|
||||
* @return array List of replacement arrays, each with a
|
||||
* 'search', 'replace' and 'limit' attribute
|
||||
*
|
||||
* @todo the types aren't correct, refactor to apply native type hinting
|
||||
*/
|
||||
public static function replacements($search, $replace, $limit): array
|
||||
{
|
||||
public static function replacements(
|
||||
$search,
|
||||
$replace,
|
||||
$limit
|
||||
): array {
|
||||
$replacements = [];
|
||||
|
||||
if (is_array($search) === true && is_array($replace) === true) {
|
||||
|
@ -768,18 +828,26 @@ class Str
|
|||
* @param array $replacements Replacement array from Str::replacements()
|
||||
* @return string String with replaced values
|
||||
*/
|
||||
public static function replaceReplacements(string $string, array $replacements): string
|
||||
{
|
||||
public static function replaceReplacements(
|
||||
string $string,
|
||||
array $replacements
|
||||
): string {
|
||||
// replace in the order of the replacements
|
||||
// behavior is identical to the official PHP str_replace()
|
||||
foreach ($replacements as $replacement) {
|
||||
if (is_int($replacement['limit']) === false) {
|
||||
throw new Exception('Invalid limit "' . $replacement['limit'] . '".');
|
||||
} elseif ($replacement['limit'] === -1) {
|
||||
}
|
||||
|
||||
if ($replacement['limit'] === -1) {
|
||||
// no limit, we don't need our special replacement routine
|
||||
$string = str_replace($replacement['search'], $replacement['replace'], $string);
|
||||
$string = str_replace(
|
||||
$replacement['search'],
|
||||
$replacement['replace'],
|
||||
$string
|
||||
);
|
||||
} elseif ($replacement['limit'] > 0) {
|
||||
// limit given, only replace for $replacement['limit'] times per replacement
|
||||
// limit given, only replace for as many times per replacement
|
||||
$position = -1;
|
||||
|
||||
for ($i = 0; $i < $replacement['limit']; $i++) {
|
||||
|
@ -886,8 +954,11 @@ class Str
|
|||
* string is too long. Ellipsis is the default.
|
||||
* @return string The shortened string
|
||||
*/
|
||||
public static function short(string $string = null, int $length = 0, string $appendix = '…'): string
|
||||
{
|
||||
public static function short(
|
||||
string $string = null,
|
||||
int $length = 0,
|
||||
string $appendix = '…'
|
||||
): string {
|
||||
if ($string === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -915,8 +986,11 @@ class Str
|
|||
* @return array matches: Number of matching chars in both strings
|
||||
* percent: Similarity in percent
|
||||
*/
|
||||
public static function similarity(string $first, string $second, bool $caseInsensitive = false): array
|
||||
{
|
||||
public static function similarity(
|
||||
string $first,
|
||||
string $second,
|
||||
bool $caseInsensitive = false
|
||||
): array {
|
||||
$matches = 0;
|
||||
$percent = 0.0;
|
||||
|
||||
|
@ -1020,11 +1094,14 @@ class Str
|
|||
/**
|
||||
* Convert a string to snake case.
|
||||
*/
|
||||
public static function snake(string $value = null, string $delimiter = '_'): string
|
||||
{
|
||||
if (!ctype_lower($value)) {
|
||||
public static function snake(
|
||||
string $value = null,
|
||||
string $delimiter = '_'
|
||||
): string {
|
||||
if (ctype_lower($value) === false) {
|
||||
$value = preg_replace('/\s+/u', '', ucwords($value));
|
||||
$value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value));
|
||||
$value = preg_replace('/(.)(?=[A-Z])/u', '$1' . $delimiter, $value);
|
||||
$value = static::lower($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
@ -1040,8 +1117,11 @@ class Str
|
|||
* @param int $length The min length of values.
|
||||
* @return array An array of found values
|
||||
*/
|
||||
public static function split(string|array|null $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;
|
||||
}
|
||||
|
@ -1065,8 +1145,11 @@ class Str
|
|||
/**
|
||||
* Checks if a string starts with the passed needle
|
||||
*/
|
||||
public static function startsWith(string $string = null, string $needle, bool $caseInsensitive = false): bool
|
||||
{
|
||||
public static function startsWith(
|
||||
string $string = null,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): bool {
|
||||
if ($needle === '') {
|
||||
return true;
|
||||
}
|
||||
|
@ -1088,8 +1171,11 @@ class Str
|
|||
/**
|
||||
* A UTF-8 safe version of substr()
|
||||
*/
|
||||
public static function substr(string $string = null, int $start = 0, int $length = null): string
|
||||
{
|
||||
public static function substr(
|
||||
string $string = null,
|
||||
int $start = 0,
|
||||
int $length = null
|
||||
): string {
|
||||
return mb_substr($string ?? '', $start, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
|
@ -1149,15 +1235,14 @@ class Str
|
|||
if ($callback !== null) {
|
||||
$callbackResult = $callback((string)$result, $query, $data);
|
||||
|
||||
if ($result === null && $callbackResult === '') {
|
||||
if ($result !== null || $callbackResult !== '') {
|
||||
// the empty string came just from string casting,
|
||||
// keep the null value and ignore the callback result
|
||||
} else {
|
||||
$result = $callbackResult;
|
||||
}
|
||||
}
|
||||
|
||||
// if we still don't have a result, keep the original placeholder
|
||||
// wihtout a result, keep the original placeholder
|
||||
return $result ?? $match[0];
|
||||
},
|
||||
$string
|
||||
|
@ -1246,8 +1331,11 @@ class Str
|
|||
/**
|
||||
* Returns the beginning of a string until the given character
|
||||
*/
|
||||
public static function until(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
public static function until(
|
||||
string $string,
|
||||
string $needle,
|
||||
bool $caseInsensitive = false
|
||||
): string {
|
||||
$position = static::position($string, $needle, $caseInsensitive);
|
||||
|
||||
if ($position === false) {
|
||||
|
@ -1304,9 +1392,11 @@ class Str
|
|||
$string ??= '';
|
||||
|
||||
// Replace space between last word and punctuation
|
||||
$string = preg_replace_callback('|(\S)\s(\S?)$|u', function ($matches) {
|
||||
return $matches[1] . ' ' . $matches[2];
|
||||
}, $string);
|
||||
$string = preg_replace_callback(
|
||||
'|(\S)\s(\S?)$|u',
|
||||
fn ($matches) => $matches[1] . ' ' . $matches[2],
|
||||
$string
|
||||
);
|
||||
|
||||
// Replace space between last two words
|
||||
return preg_replace_callback('|(\s)(?=\S*$)(\S+)|u', function ($matches) {
|
||||
|
@ -1325,8 +1415,11 @@ class Str
|
|||
* @param string $before String to prepend
|
||||
* @param string|null $after String to append (if different from `$before`)
|
||||
*/
|
||||
public static function wrap(string $string, string $before, string $after = null): string
|
||||
{
|
||||
public static function wrap(
|
||||
string $string,
|
||||
string $before,
|
||||
string $after = null
|
||||
): string {
|
||||
return $before . $string . ($after ?? $before);
|
||||
}
|
||||
}
|
||||
|
|
216
kirby/src/Toolkit/SymmetricCrypto.php
Normal file
216
kirby/src/Toolkit/SymmetricCrypto.php
Normal file
|
@ -0,0 +1,216 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Kirby\Data\Json;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use SensitiveParameter;
|
||||
|
||||
/**
|
||||
* User-friendly and safe abstraction for symmetric
|
||||
* authenticated encryption and decryption using the
|
||||
* PHP `sodium` extension
|
||||
* @since 3.9.8
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class SymmetricCrypto
|
||||
{
|
||||
/**
|
||||
* Cache for secret keys derived from the password
|
||||
* indexed by the used salt and limits
|
||||
*/
|
||||
protected array $secretKeysByOptions = [];
|
||||
|
||||
/**
|
||||
* Initializes the keys used for crypto, both optional
|
||||
*
|
||||
* @param string|null $password Password to be derived into a `$secretKey`
|
||||
* @param string|null $secretKey 256-bit key, alternatively a `$password` can be used
|
||||
*/
|
||||
public function __construct(
|
||||
#[SensitiveParameter]
|
||||
protected string|null $password = null,
|
||||
#[SensitiveParameter]
|
||||
protected string|null $secretKey = null,
|
||||
) {
|
||||
if ($password !== null && $secretKey !== null) {
|
||||
throw new InvalidArgumentException('Passing both a secret key and a password is not supported');
|
||||
}
|
||||
|
||||
if ($secretKey !== null && strlen($secretKey) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
|
||||
throw new InvalidArgumentException('Invalid secret key length, expected ' . SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide values of secrets when printing the object
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return [
|
||||
'hasPassword' => isset($this->password),
|
||||
'hasSecretKey' => isset($this->secretKey),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes the secrets from memory when they are no longer needed
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->memzero($this->password);
|
||||
$this->memzero($this->secretKey);
|
||||
|
||||
foreach ($this->secretKeysByOptions as $key => &$value) {
|
||||
$this->memzero($value);
|
||||
unset($this->secretKeysByOptions[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts JSON data encrypted by `SymmetricCrypto::encrypt()` using the secret key or password
|
||||
*
|
||||
* <code>
|
||||
* // decryption with a password
|
||||
* $crypto = new SymmetricCrypto(password: 'super secure');
|
||||
* $plaintext = $crypto->decrypt('a very confidential string');
|
||||
*
|
||||
* // decryption with a previously generated key
|
||||
* $crypto = new SymmetricCrypto(secretKey: $secretKey);
|
||||
* $plaintext = $crypto->decrypt('{"mode":"secretbox"...}');
|
||||
* </code>
|
||||
*/
|
||||
public function decrypt(string $json): string
|
||||
{
|
||||
$props = Json::decode($json);
|
||||
|
||||
if (($props['mode'] ?? null) !== 'secretbox') {
|
||||
throw new InvalidArgumentException('Unsupported encryption mode "' . ($props['mode'] ?? '') . '"');
|
||||
}
|
||||
|
||||
if (
|
||||
isset($props['data']) !== true ||
|
||||
isset($props['nonce']) !== true ||
|
||||
isset($props['salt']) !== true ||
|
||||
isset($props['limits']) !== true
|
||||
) {
|
||||
throw new InvalidArgumentException('Input data does not contain all required props');
|
||||
}
|
||||
|
||||
$data = base64_decode($props['data']);
|
||||
$nonce = base64_decode($props['nonce']);
|
||||
$salt = base64_decode($props['salt']);
|
||||
$limits = $props['limits'];
|
||||
|
||||
$plaintext = sodium_crypto_secretbox_open($data, $nonce, $this->secretKey($salt, $limits));
|
||||
|
||||
if (is_string($plaintext) !== true) {
|
||||
throw new LogicException('Encrypted string was tampered with');
|
||||
}
|
||||
|
||||
return $plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a string using the secret key or password
|
||||
*
|
||||
* <code>
|
||||
* // encryption with a password
|
||||
* $crypto = new SymmetricCrypto(password: 'super secure');
|
||||
* $ciphertext = $crypto->encrypt('a very confidential string');
|
||||
*
|
||||
* // encryption with a random key
|
||||
* $crypto = new SymmetricCrypto();
|
||||
* $ciphertext = $crypto->encrypt('a very confidential string');
|
||||
* $secretKey = $crypto->secretKey();
|
||||
* </code>
|
||||
*/
|
||||
public function encrypt(
|
||||
#[SensitiveParameter]
|
||||
string $string
|
||||
): string {
|
||||
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||
$salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES);
|
||||
$limits = [SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE];
|
||||
$key = $this->secretKey($salt, $limits);
|
||||
|
||||
$ciphertext = sodium_crypto_secretbox($string, $nonce, $key);
|
||||
|
||||
// bundle all necessary information in a JSON object;
|
||||
// always include the salt and limits to hide whether a key or password was used
|
||||
return Json::encode([
|
||||
'mode' => 'secretbox',
|
||||
'data' => base64_encode($ciphertext),
|
||||
'nonce' => base64_encode($nonce),
|
||||
'salt' => base64_encode($salt),
|
||||
'limits' => $limits,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the required PHP `sodium` extension is available
|
||||
*/
|
||||
public static function isAvailable(): bool
|
||||
{
|
||||
return defined('SODIUM_LIBRARY_MAJOR_VERSION') === true && SODIUM_LIBRARY_MAJOR_VERSION >= 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the binary secret key, optionally derived from the password
|
||||
* or randomly generated
|
||||
*
|
||||
* @param string|null $salt Salt for password-based key derivation
|
||||
* @param array|null $limits Processing limits for password-based key derivation
|
||||
*/
|
||||
public function secretKey(
|
||||
#[SensitiveParameter]
|
||||
string|null $salt = null,
|
||||
array|null $limits = null
|
||||
): string {
|
||||
if (isset($this->secretKey) === true) {
|
||||
return $this->secretKey;
|
||||
}
|
||||
|
||||
// derive from password
|
||||
if (isset($this->password) === true) {
|
||||
if ($salt === null || $limits === null) {
|
||||
throw new InvalidArgumentException('Salt and limits are required when deriving a secret key from a password');
|
||||
}
|
||||
|
||||
// access from cache
|
||||
$options = $salt . ':' . implode(',', $limits);
|
||||
if (isset($this->secretKeysByOptions[$options]) === true) {
|
||||
return $this->secretKeysByOptions[$options];
|
||||
}
|
||||
|
||||
return $this->secretKeysByOptions[$options] = sodium_crypto_pwhash(
|
||||
SODIUM_CRYPTO_SECRETBOX_KEYBYTES,
|
||||
$this->password,
|
||||
$salt,
|
||||
$limits[0],
|
||||
$limits[1],
|
||||
SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
|
||||
);
|
||||
}
|
||||
|
||||
// generate a random key
|
||||
return $this->secretKey = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes a variable from memory if it is a string
|
||||
*/
|
||||
protected function memzero(mixed &$value): void
|
||||
{
|
||||
if (is_string($value) === true) {
|
||||
sodium_memzero($value);
|
||||
$value = '';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,8 +21,10 @@ class Tpl
|
|||
*
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function load(string|null $file = null, array $data = []): string
|
||||
{
|
||||
public static function load(
|
||||
string|null $file = null,
|
||||
array $data = []
|
||||
): string {
|
||||
if ($file === null || is_file($file) === false) {
|
||||
return '';
|
||||
}
|
||||
|
@ -39,10 +41,10 @@ class Tpl
|
|||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
if ($exception === null) {
|
||||
return $content;
|
||||
if ($exception !== null) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,8 +32,11 @@ class V
|
|||
* and returns an array with all error messages.
|
||||
* The array will be empty if the input is valid
|
||||
*/
|
||||
public static function errors($input, array $rules, array $messages = []): array
|
||||
{
|
||||
public static function errors(
|
||||
$input,
|
||||
array $rules,
|
||||
array $messages = []
|
||||
): array {
|
||||
$errors = static::value($input, $rules, $messages, false);
|
||||
|
||||
return $errors === true ? [] : $errors;
|
||||
|
@ -44,8 +47,11 @@ class V
|
|||
* checks if the data is invalid
|
||||
* @since 3.7.0
|
||||
*/
|
||||
public static function invalid(array $data = [], array $rules = [], array $messages = []): array
|
||||
{
|
||||
public static function invalid(
|
||||
array $data = [],
|
||||
array $rules = [],
|
||||
array $messages = []
|
||||
): array {
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $field => $validations) {
|
||||
|
@ -108,8 +114,10 @@ class V
|
|||
* and the arguments. This is used mainly internally
|
||||
* to create error messages
|
||||
*/
|
||||
public static function message(string $validatorName, ...$params): string|null
|
||||
{
|
||||
public static function message(
|
||||
string $validatorName,
|
||||
...$params
|
||||
): string|null {
|
||||
$validatorName = strtolower($validatorName);
|
||||
$translationKey = 'error.validation.' . $validatorName;
|
||||
$validators = array_change_key_case(static::$validators);
|
||||
|
@ -157,8 +165,12 @@ class V
|
|||
* a set of rules, using all registered
|
||||
* validators
|
||||
*/
|
||||
public static function value($value, array $rules, array $messages = [], bool $fail = true): bool|array
|
||||
{
|
||||
public static function value(
|
||||
$value,
|
||||
array $rules,
|
||||
array $messages = [],
|
||||
bool $fail = true
|
||||
): bool|array {
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $validatorName => $validatorOptions) {
|
||||
|
|
|
@ -17,23 +17,14 @@ use Throwable;
|
|||
*/
|
||||
class View
|
||||
{
|
||||
/**
|
||||
* The absolute path to the view file
|
||||
*/
|
||||
protected string $file;
|
||||
|
||||
/**
|
||||
* The view data
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* Creates a new view object
|
||||
*/
|
||||
public function __construct(string $file, array $data = [])
|
||||
{
|
||||
$this->file = $file;
|
||||
$this->data = $data;
|
||||
public function __construct(
|
||||
// The absolute path to the view file
|
||||
protected string $file,
|
||||
protected array $data = []
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -70,8 +70,10 @@ 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(string|array $name, $value = null): string|null
|
||||
{
|
||||
public static function attr(
|
||||
string|array $name,
|
||||
$value = null
|
||||
): string|null {
|
||||
if (is_array($name) === true) {
|
||||
if ($value !== false) {
|
||||
ksort($name);
|
||||
|
@ -235,8 +237,10 @@ class Xml
|
|||
*
|
||||
* @param bool $html True = Convert to HTML-safe first
|
||||
*/
|
||||
public static function encode(string|null $string, bool $html = true): string
|
||||
{
|
||||
public static function encode(
|
||||
string|null $string,
|
||||
bool $html = true
|
||||
): string {
|
||||
if ($string === null) {
|
||||
return '';
|
||||
}
|
||||
|
@ -267,7 +271,7 @@ class Xml
|
|||
*/
|
||||
public static function parse(string $xml): array|null
|
||||
{
|
||||
$xml = @simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);
|
||||
$xml = @simplexml_load_string($xml);
|
||||
|
||||
if (is_object($xml) !== true) {
|
||||
return null;
|
||||
|
@ -282,8 +286,10 @@ class Xml
|
|||
*
|
||||
* @param bool $collectName Whether the element name should be collected (for the root element)
|
||||
*/
|
||||
public static function simplify(SimpleXMLElement $element, bool $collectName = true): array|string
|
||||
{
|
||||
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
|
||||
$usedNamespaces = $element->getNamespaces(true);
|
||||
|
@ -347,18 +353,18 @@ class Xml
|
|||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
} else {
|
||||
// we didn't find any XML children above, only use the string value
|
||||
$element = (string)$element;
|
||||
|
||||
if (count($array) === 0) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
$array['@value'] = $element;
|
||||
return $array;
|
||||
}
|
||||
|
||||
// we didn't find any XML children above, only use the string value
|
||||
$element = (string)$element;
|
||||
|
||||
if (count($array) === 0) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
$array['@value'] = $element;
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -372,8 +378,13 @@ class Xml
|
|||
* @param int $level Indentation level
|
||||
* @return string The generated XML
|
||||
*/
|
||||
public static function tag(string $name, $content = '', array $attr = [], string $indent = null, int $level = 0): string
|
||||
{
|
||||
public static function tag(
|
||||
string $name,
|
||||
array|string|null $content = '',
|
||||
array $attr = [],
|
||||
string $indent = null,
|
||||
int $level = 0
|
||||
): string {
|
||||
$attr = static::attr($attr);
|
||||
$start = '<' . $name . ($attr ? ' ' . $attr : '') . '>';
|
||||
$startShort = '<' . $name . ($attr ? ' ' . $attr : '') . static::$void;
|
||||
|
|
|
@ -46,7 +46,7 @@ class SiteUuid extends Uuid
|
|||
/**
|
||||
* Pretends to fill cache - we don't need it in cache
|
||||
*/
|
||||
public function populate(): bool
|
||||
public function populate(bool $force = false): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ class UserUuid extends Uuid
|
|||
/**
|
||||
* Pretends to fill cache - we don't need it in cache
|
||||
*/
|
||||
public function populate(): bool
|
||||
public function populate(bool $force = false): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -309,7 +309,8 @@ class Uuid
|
|||
// lazily fill cache by writing to cache
|
||||
// whenever looked up from index to speed
|
||||
// up future lookups of the same UUID
|
||||
$this->populate();
|
||||
// also force to update value again if it is already cached
|
||||
$this->populate($this->isCached());
|
||||
|
||||
return $this->model;
|
||||
}
|
||||
|
@ -320,12 +321,10 @@ class Uuid
|
|||
|
||||
/**
|
||||
* Feeds the UUID into the cache
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function populate(): bool
|
||||
public function populate(bool $force = false): bool
|
||||
{
|
||||
if ($this->isCached() === true) {
|
||||
if ($force === false && $this->isCached() === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue