Update Composer packages

This commit is contained in:
Paul Nicoué 2024-12-20 12:37:52 +01:00
parent 9252d9ce90
commit 134266af8a
176 changed files with 7930 additions and 2262 deletions

View file

@ -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);
}

View file

@ -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)

View file

@ -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) {

View file

@ -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());
}
}

View file

@ -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;
}
/**

View file

@ -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());

View file

@ -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 = [])
{

View file

@ -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',

View file

@ -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)
};

View file

@ -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) {

View file

@ -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)
{

View file

@ -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

View file

@ -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;

View file

@ -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()) {

View file

@ -44,7 +44,8 @@ class Permissions
],
'languages' => [
'create' => true,
'delete' => true
'delete' => true,
'update' => true
],
'pages' => [
'changeSlug' => true,

View file

@ -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;
}

View file

@ -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';
}
}

View file

@ -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);
}
}

View file

@ -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;
}