Upgrade to 4.5.0

This commit is contained in:
Bastian Allgeier
2024-11-28 11:24:28 +01:00
parent 49287c7a5e
commit 63ddf40692
86 changed files with 942 additions and 491 deletions

View File

@@ -338,13 +338,12 @@ class File extends ModelWithContent
return false;
}
static $accessible = [];
static $accessible = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->template() ?? '__none__';
$accessible[$role] ??= [];
if ($template = $this->template()) {
return $accessible[$template] ??= $this->permissions()->can('access');
}
return $accessible['__none__'] ??= $this->permissions()->can('access');
return $accessible[$role][$template] ??= $this->permissions()->can('access');
}
/**
@@ -363,13 +362,12 @@ class File extends ModelWithContent
return false;
}
static $listable = [];
static $listable = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->template() ?? '__none__';
$listable[$role] ??= [];
if ($template = $this->template()) {
return $listable[$template] ??= $this->permissions()->can('list');
}
return $listable['__none__'] ??= $this->permissions()->can('list');
return $listable[$role][$template] ??= $this->permissions()->can('list');
}
/**
@@ -379,13 +377,12 @@ class File extends ModelWithContent
*/
public function isReadable(): bool
{
static $readable = [];
static $readable = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->template() ?? '__none__';
$readable[$role] ??= [];
if ($template = $this->template()) {
return $readable[$template] ??= $this->permissions()->can('read');
}
return $readable['__none__'] ??= $this->permissions()->can('read');
return $readable[$role][$template] ??= $this->permissions()->can('read');
}
/**
@@ -633,12 +630,6 @@ class File extends ModelWithContent
case 'page':
$preview = $parent->blueprint()->preview();
// user has no permission to preview page,
// also return null for file preview
if ($preview === false) {
return null;
}
// the page has a custom preview setting,
// thus the file is only accessible through
// the direct media URL

View File

@@ -139,10 +139,9 @@ trait FileActions
$file = $file->update(['template' => $template]);
// rename and/or resize the file if configured by new blueprint
// resize the file if configured by new blueprint
$create = $file->blueprint()->create();
$file = $file->manipulate($create);
$file = $file->changeExtension($file, $create['format'] ?? null);
$file = $file->manipulate($create);
return $file;
});
@@ -185,6 +184,7 @@ trait FileActions
/**
* Copy the file to the given page
* @internal
*/
public function copy(Page $page): static
{
@@ -285,7 +285,6 @@ trait FileActions
// resize the file on upload if configured
$file = $file->manipulate($create);
$file = $file->changeExtension($file, $create['format'] ?? null);
// store the content if necessary
// (always create files in the default language)
@@ -338,7 +337,14 @@ trait FileActions
// generate image file and overwrite it in place
$this->kirby()->thumb($this->root(), $this->root(), $options);
return $this->clone([]);
$file = $this->clone();
// change the file extension if format option configured
if ($format = $options['format'] ?? null) {
$file = $file->changeExtension($file, $format);
}
return $file;
}
/**
@@ -387,7 +393,6 @@ trait FileActions
// apply the resizing/crop options from the blueprint
$create = $file->blueprint()->create();
$file = $file->manipulate($create);
$file = $file->changeExtension($file, $create['format'] ?? null);
// return a fresh clone
return $file->clone();

View File

@@ -56,7 +56,7 @@ class LanguageVariable
throw new DuplicateException('The variable is part of the core translation and cannot be overwritten');
}
$translations[$key] = trim($value ?? '');
$translations[$key] = $value ?? '';
$language->update(['translations' => $translations]);
@@ -102,10 +102,10 @@ class LanguageVariable
/**
* Sets a new value for the language variable
*/
public function update(string $value): static
public function update(string|null $value = null): static
{
$translations = $this->language->translations();
$translations[$this->key] = $value;
$translations = $this->language->translations();
$translations[$this->key] = $value ?? '';
$language = $this->language->update(['translations' => $translations]);

View File

@@ -42,8 +42,9 @@ class License
protected string|null $date = null,
protected string|null $signature = null,
) {
// normalize the email address
$this->email = $this->email === null ? null : $this->normalizeEmail($this->email);
// normalize arguments
$this->code = $this->code !== null ? trim($this->code) : null;
$this->email = $this->email !== null ? $this->normalizeEmail($this->email) : null;
}
/**

View File

@@ -523,11 +523,12 @@ class Page extends ModelWithContent
return false;
}
static $accessible = [];
static $accessible = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->intendedTemplate()->name();
$accessible[$role] ??= [];
$template = $this->intendedTemplate()->name();
return $accessible[$template] ??= $this->permissions()->can('access');
return $accessible[$role][$template] ??= $this->permissions()->can('access');
}
/**
@@ -689,11 +690,12 @@ class Page extends ModelWithContent
return false;
}
static $listable = [];
static $listable = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->intendedTemplate()->name();
$listable[$role] ??= [];
$template = $this->intendedTemplate()->name();
return $listable[$template] ??= $this->permissions()->can('list');
return $listable[$role][$template] ??= $this->permissions()->can('list');
}
/**
@@ -745,11 +747,12 @@ class Page extends ModelWithContent
*/
public function isReadable(): bool
{
static $readable = [];
static $readable = [];
$role = $this->kirby()->user()?->role()->id() ?? '__none__';
$template = $this->intendedTemplate()->name();
$readable[$role] ??= [];
$template = $this->intendedTemplate()->name();
return $readable[$template] ??= $this->permissions()->can('read');
return $readable[$role][$template] ??= $this->permissions()->can('read');
}
/**

View File

@@ -153,7 +153,7 @@ trait PageActions
string|null $languageCode = null
): static {
// always sanitize the slug
$slug = Str::slug($slug);
$slug = Url::slug($slug);
// in multi-language installations the slug for the non-default
// languages is stored in the text file. The changeSlugForLanguage
@@ -431,6 +431,8 @@ trait PageActions
* Copies the page to a new parent
*
* @throws \Kirby\Exception\DuplicateException If the page already exists
*
* @internal
*/
public function copy(array $options = []): static
{
@@ -443,7 +445,7 @@ trait PageActions
$files = $options['files'] ?? false;
// clean up the slug
$slug = Str::slug($slug);
$slug = Url::slug($slug);
if ($parentModel->findPageOrDraft($slug)) {
throw new DuplicateException([
@@ -495,7 +497,7 @@ trait PageActions
public static function create(array $props): Page
{
// clean up the slug
$props['slug'] = Str::slug($props['slug'] ?? $props['content']['title'] ?? null);
$props['slug'] = Url::slug($props['slug'] ?? $props['content']['title'] ?? null);
$props['template'] = $props['model'] = strtolower($props['template'] ?? 'default');
$props['isDraft'] ??= $props['draft'] ?? true;
@@ -685,7 +687,7 @@ trait PageActions
public function duplicate(string|null $slug = null, array $options = []): static
{
// create the slug for the duplicate
$slug = Str::slug($slug ?? $this->slug() . '-' . Str::slug(I18n::translate('page.duplicate.appendix')));
$slug = Url::slug($slug ?? $this->slug() . '-' . Url::slug(I18n::translate('page.duplicate.appendix')));
$arguments = [
'originalPage' => $this,
@@ -762,6 +764,7 @@ trait PageActions
/**
* @return $this|static
* @throws \Kirby\Exception\LogicException If the folder cannot be moved
* @internal
*/
public function publish(): static
{
@@ -913,6 +916,7 @@ trait PageActions
/**
* Convert a page from listed or
* unlisted to draft.
* @internal
*
* @return $this|static
* @throws \Kirby\Exception\LogicException If the folder cannot be moved

View File

@@ -365,27 +365,40 @@ class PageRules
$allowed = [];
// collect all allowed subpage templates
foreach ($parent->blueprint()->sections() as $section) {
// only take pages sections into consideration
if ($section->type() !== 'pages') {
continue;
}
// from all pages sections in the blueprint
// (only consider page sections that list pages
// of the targeted new parent page)
$sections = array_filter(
$parent->blueprint()->sections(),
fn ($section) =>
$section->type() === 'pages' &&
$section->parent()->is($parent)
);
// only consider page sections that list pages
// of the targeted new parent page
if ($section->parent() !== $parent) {
continue;
}
// check if the parent has at least one pages section
if ($sections === []) {
throw new LogicException([
'key' => 'page.move.noSections',
'data' => [
'parent' => $parent->id() ?? '/',
]
]);
}
// go through all allowed blueprints and
// add the name to the allow list
foreach ($section->blueprints() as $blueprint) {
$allowed[] = $blueprint['name'];
// go through all allowed templates and
// add the name to the allowlist
foreach ($sections as $section) {
foreach ($section->templates() as $template) {
$allowed[] = $template;
}
}
// check if the template of this page is allowed as subpage type
if (in_array($page->intendedTemplate()->name(), $allowed) === false) {
// for the potential new parent
if (
$allowed !== [] &&
in_array($page->intendedTemplate()->name(), $allowed) === false
) {
throw new PermissionException([
'key' => 'page.move.template',
'data' => [

View File

@@ -25,15 +25,19 @@ class Roles extends Collection
/**
* Returns a filtered list of all
* roles that can be created by the
* roles that can be changed by the
* current user
*
* Use with `$kirby->roles()`. For retrieving
* which roles are available for a specific user,
* use `$user->roles()` without additional filters.
*
* @return $this|static
* @throws \Exception
*/
public function canBeChanged(): static
{
if (App::instance()->user()) {
if (App::instance()->user()?->isAdmin() !== true) {
return $this->filter(function ($role) {
$newUser = new User([
'email' => 'test@getkirby.com',
@@ -50,14 +54,16 @@ class Roles extends Collection
/**
* Returns a filtered list of all
* roles that can be created by the
* current user
* current user.
*
* Use with `$kirby->roles()`.
*
* @return $this|static
* @throws \Exception
*/
public function canBeCreated(): static
{
if (App::instance()->user()) {
if (App::instance()->user()?->isAdmin() !== true) {
return $this->filter(function ($role) {
$newUser = new User([
'email' => 'test@getkirby.com',

View File

@@ -26,7 +26,7 @@ class UpdateStatus
/**
* Host to request the update data from
*/
public static string $host = 'https://assets.getkirby.com';
public static string $host = 'https://getkirby.com';
/**
* Marker that stores whether a previous remote

View File

@@ -3,6 +3,7 @@
namespace Kirby\Cms;
use Kirby\Http\Url as BaseUrl;
use Kirby\Toolkit\Str;
/**
* The `Url` class extends the
@@ -31,6 +32,26 @@ class Url extends BaseUrl
return App::instance()->url();
}
/**
* Convert a string to a safe version to be used in a URL,
* obeying the `slugs.maxlength` option
*
* @param string $string The unsafe string
* @param string $separator To be used instead of space and
* other non-word characters.
* @param string $allowed List of all allowed characters (regex)
* @param int $maxlength The maximum length of the slug
* @return string The safe string
*/
public static function slug(
string $string = null,
string $separator = null,
string $allowed = null,
): string {
$maxlength = App::instance()->option('slugs.maxlength', 255);
return Str::slug($string, $separator, $allowed, $maxlength);
}
/**
* Creates an absolute Url to a template asset if it exists.
* This is used in the `css()` and `js()` helpers

View File

@@ -574,33 +574,24 @@ class User extends ModelWithContent
}
/**
* Returns all available roles
* for this user, that can be selected
* by the authenticated user
* Returns all available roles for this user,
* that the authenticated user can change to.
*
* For all roles the current user can create
* use `$kirby->roles()->canBeCreated()`.
*/
public function roles(): Roles
{
$kirby = $this->kirby();
$roles = $kirby->roles();
// a collection with just the one role of the user
$myRole = $roles->filter('id', $this->role()->id());
// if there's an authenticated user …
// admin users can select pretty much any role
if ($kirby->user()?->isAdmin() === true) {
// except if the user is the last admin
if ($this->isLastAdmin() === true) {
// in which case they have to stay admin
return $myRole;
}
// return all roles for mighty admins
return $roles;
// if the authenticated user doesn't have the permission to change
// the role of this user, only the current role is available
if ($this->permissions()->can('changeRole') === false) {
return $roles->filter('id', $this->role()->id());
}
// any other user can only keep their role
return $myRole;
return $roles->canBeCreated();
}
/**
@@ -666,7 +657,7 @@ class User extends ModelWithContent
*/
protected function siblingsCollection(): Users
{
return $this->kirby()->users();
return $this->kirby()->users()->sortBy('username', 'asc');
}
/**

View File

@@ -26,7 +26,20 @@ class UserPermissions extends ModelPermissions
protected function canChangeRole(): bool
{
return $this->model->roles()->count() > 1;
// protect admin from role changes by non-admin
if (
$this->model->isAdmin() === true &&
$this->user->isAdmin() !== true
) {
return false;
}
// prevent demoting the last admin
if ($this->model->isLastAdmin() === true) {
return false;
}
return true;
}
protected function canCreate(): bool

View File

@@ -101,17 +101,6 @@ class UserRules
*/
public static function changeRole(User $user, string $role): bool
{
// protect admin from role changes by non-admin
if (
$user->kirby()->user()->isAdmin() === false &&
$user->isAdmin() === true
) {
throw new PermissionException([
'key' => 'user.changeRole.permission',
'data' => ['name' => $user->username()]
]);
}
// prevent non-admins making a user to admin
if (
$user->kirby()->user()->isAdmin() === false &&
@@ -122,8 +111,7 @@ class UserRules
]);
}
static::validRole($user, $role);
// prevent demoting the last admin
if ($role !== 'admin' && $user->isLastAdmin() === true) {
throw new LogicException([
'key' => 'user.changeRole.lastAdmin',
@@ -131,6 +119,7 @@ class UserRules
]);
}
// check permissions
if ($user->permissions()->changeRole() !== true) {
throw new PermissionException([
'key' => 'user.changeRole.permission',
@@ -138,6 +127,13 @@ class UserRules
]);
}
// prevent changing to role that is not available for user
if ($user->roles()->find($role) instanceof Role === false) {
throw new InvalidArgumentException([
'key' => 'user.role.invalid',
]);
}
return true;
}
@@ -199,22 +195,27 @@ class UserRules
return true;
}
// only admins are allowed to add admins
$role = $props['role'] ?? null;
// allow to create the first user
if ($user->kirby()->users()->count() === 0) {
return true;
}
if ($role === 'admin' && $currentUser?->isAdmin() === false) {
// check user permissions
if ($user->permissions()->create() !== true) {
throw new PermissionException([
'key' => 'user.create.permission'
]);
}
// check user permissions (if not on install)
$role = $props['role'] ?? null;
// prevent creating a role that is not available for user
if (
$user->kirby()->users()->count() > 0 &&
$user->permissions()->create() !== true
in_array($role, [null, 'default', 'nobody'], true) === false &&
$user->kirby()->roles()->canBeCreated()->find($role) instanceof Role === false
) {
throw new PermissionException([
'key' => 'user.create.permission'
throw new InvalidArgumentException([
'key' => 'user.role.invalid',
]);
}
@@ -370,6 +371,7 @@ class UserRules
* Validates a user role
*
* @throws \Kirby\Exception\InvalidArgumentException If the user role does not exist
* @deprecated 4.5.0
*/
public static function validRole(User $user, string $role): bool
{