Upgrade to 4.5.0
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user