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
{

View File

@@ -124,9 +124,17 @@ class Field
*/
public function isEmpty(): bool
{
$value = $this->value;
if (is_string($value) === true) {
$value = trim($value);
}
return
empty($this->value) === true &&
in_array($this->value, [0, '0', false], true) === false;
$value === null ||
$value === '' ||
$value === [] ||
$value === '[]';
}
/**

View File

@@ -28,6 +28,7 @@ class Image extends File
protected Dimensions|null $dimensions = null;
public static array $resizableTypes = [
'avif',
'jpg',
'jpeg',
'gif',

View File

@@ -28,7 +28,9 @@ class OptionsApi extends OptionsProvider
public string $url,
public string|null $query = null,
public string|null $text = null,
public string|null $value = null
public string|null $value = null,
public string|null $icon = null,
public string|null $info = null
) {
}
@@ -46,10 +48,12 @@ class OptionsApi extends OptionsProvider
}
return new static(
url: $props['url'],
url : $props['url'],
query: $props['query'] ?? $props['fetch'] ?? null,
text: $props['text'] ?? null,
value: $props['value'] ?? null
text : $props['text'] ?? null,
value: $props['value'] ?? null,
icon : $props['icon'] ?? null,
info : $props['info'] ?? null
);
}
@@ -138,7 +142,10 @@ class OptionsApi extends OptionsProvider
'value' => $model->toString($this->value, ['item' => $item]),
// text is only a raw string when using {< >}
// or when the safe mode is explicitly disabled (select field)
'text' => $model->$safeMethod($this->text, ['item' => $item])
'text' => $model->$safeMethod($this->text, ['item' => $item]),
// additional data
'icon' => $this->icon !== null ? $model->toString($this->icon, ['item' => $item]) : null,
'info' => $this->info !== null ? $model->$safeMethod($this->info, ['item' => $item]) : null
];
}

View File

@@ -30,7 +30,9 @@ class OptionsQuery extends OptionsProvider
public function __construct(
public string $query,
public string|null $text = null,
public string|null $value = null
public string|null $value = null,
public string|null $icon = null,
public string|null $info = null
) {
}
@@ -56,8 +58,10 @@ class OptionsQuery extends OptionsProvider
return new static(
query: $props['query'] ?? $props['fetch'],
text: $props['text'] ?? null,
value: $props['value'] ?? null
text : $props['text'] ?? null,
value: $props['value'] ?? null,
icon : $props['icon'] ?? null,
info : $props['info'] ?? null
);
}
@@ -178,7 +182,11 @@ class OptionsQuery extends OptionsProvider
$safeMethod = $safeMode === true ? 'toSafeString' : 'toString';
$text = $model->$safeMethod($this->text ?? $text, $data);
return compact('text', 'value');
// additional data
$icon = $this->icon !== null ? $model->toString($this->icon, $data) : null;
$info = $this->info !== null ? $model->$safeMethod($this->info, $data) : null;
return compact('text', 'value', 'icon', 'info');
});
return $this->options = Options::factory($options);

View File

@@ -6,6 +6,7 @@ use Kirby\Cms\App;
use Kirby\Cms\File;
use Kirby\Cms\ModelWithContent;
use Kirby\Cms\Page;
use Kirby\Cms\Roles;
use Kirby\Form\Form;
use Kirby\Http\Router;
use Kirby\Toolkit\I18n;
@@ -191,29 +192,33 @@ class Field
/**
* User role radio buttons
*/
public static function role(array $props = []): array
{
$kirby = App::instance();
$isAdmin = $kirby->user()?->isAdmin() ?? false;
$roles = [];
public static function role(
array $props = [],
Roles|null $roles = null
): array {
$kirby = App::instance();
foreach ($kirby->roles() as $role) {
// exclude the admin role, if the user
// is not allowed to change role to admin
if ($role->name() === 'admin' && $isAdmin === false) {
continue;
}
// if no $roles where provided, fall back to all roles
$roles ??= $kirby->roles();
$roles[] = [
'text' => $role->title(),
'info' => $role->description() ?? I18n::translate('role.description.placeholder'),
'value' => $role->name()
];
}
// exclude the admin role, if the user
// is not allowed to change role to admin
$roles = $roles->filter(
fn ($role) =>
$role->name() !== 'admin' ||
$kirby->user()?->isAdmin() === true
);
// turn roles into radio field options
$roles = $roles->values(fn ($role) => [
'text' => $role->title(),
'info' => $role->description() ?? I18n::translate('role.description.placeholder'),
'value' => $role->name()
]);
return array_merge([
'label' => I18n::translate('role'),
'type' => count($roles) <= 1 ? 'hidden' : 'radio',
'type' => count($roles) < 1 ? 'hidden' : 'radio',
'options' => $roles
], $props);
}

View File

@@ -54,6 +54,7 @@ class PageCreateDialog
'tags',
'tel',
'text',
'toggle',
'toggles',
'time',
'url'
@@ -247,8 +248,9 @@ class PageCreateDialog
*/
public function model(): Page
{
// TODO: use actual in-memory page in v5
return $this->model ??= Page::factory([
'slug' => 'new',
'slug' => '__new__',
'template' => $this->template,
'model' => $this->template,
'parent' => $this->parent instanceof Page ? $this->parent : null
@@ -266,12 +268,7 @@ class PageCreateDialog
// create temporary page object
// to resolve the template strings
$page = new Page([
'slug' => 'tmp',
'template' => $this->template,
'parent' => $this->model(),
'content' => $input
]);
$page = $this->model()->clone(['content' => $input]);
if (is_string($title)) {
$input['title'] = $page->toSafeString($title);

View File

@@ -70,7 +70,7 @@ class User extends Model
'dialog' => $url . '/changeRole',
'icon' => 'bolt',
'text' => I18n::translate('user.changeRole'),
'disabled' => $this->isDisabledDropdownOption('changeRole', $options, $permissions)
'disabled' => $this->isDisabledDropdownOption('changeRole', $options, $permissions) || $this->model->roles()->count() < 2
];
$result[] = [
@@ -218,14 +218,19 @@ class User extends Model
*/
public function props(): array
{
$user = $this->model;
$account = $user->isLoggedIn();
$user = $this->model;
$account = $user->isLoggedIn();
$permissions = $this->options();
return array_merge(
parent::props(),
$account ? [] : $this->prevNext(),
$this->prevNext(),
[
'blueprint' => $this->model->role()->name(),
'blueprint' => $this->model->role()->name(),
'canChangeEmail' => $permissions['changeEmail'],
'canChangeLanguage' => $permissions['changeLanguage'],
'canChangeName' => $permissions['changeName'],
'canChangeRole' => $this->model->roles()->count() > 1,
'model' => [
'account' => $account,
'avatar' => $user->avatar()?->url(),

View File

@@ -275,6 +275,9 @@ class View
return [
'$config' => fn () => [
'api' => [
'methodOverwrite' => $kirby->option('api.methodOverwrite', true)
],
'debug' => $kirby->option('debug', false),
'kirbytext' => $kirby->option('panel.kirbytext', true),
'translation' => $kirby->option('panel.language', 'en'),

View File

@@ -96,8 +96,6 @@ class Collection extends Iterator implements Countable
* Low-level setter for elements
*
* @param string $key string or array
* @param mixed $value
* @return void
*/
public function __set(string $key, $value): void
{

View File

@@ -102,4 +102,12 @@ class Obj extends stdClass
{
return json_encode($this->toArray(), ...$arguments);
}
/**
* Returns the property names as keys
*/
public function toKeys(): array
{
return array_keys((array)$this);
}
}

View File

@@ -1132,7 +1132,7 @@ class Str
string $string = null,
string $separator = null,
string $allowed = null,
int $maxlength = 128
int|false $maxlength = 128
): string {
$separator ??= static::$defaults['slug']['separator'];
$allowed ??= static::$defaults['slug']['allowed'];
@@ -1165,7 +1165,11 @@ class Str
$string = preg_replace('![^a-z0-9]+$!', '', $string);
// cut the string after the given maxlength
return static::short($string, $maxlength, '');
if ($maxlength !== false) {
$string = static::short($string, $maxlength, '');
}
return $string;
}
/**

View File

@@ -3,6 +3,7 @@
namespace Kirby\Uuid;
use Generator;
use Kirby\Cms\App;
use Kirby\Cms\File;
/**
@@ -83,4 +84,18 @@ class FileUuid extends ModelUuid
'filename' => $model->filename()
];
}
/**
* Returns permalink url
*/
public function url(): string
{
// make sure UUID is cached because the permalink
// route only looks up UUIDs from cache
if ($this->isCached() === false) {
$this->populate();
}
return App::instance()->url() . '/@/' . static::TYPE . '/' . $this->id();
}
}

View File

@@ -2,8 +2,6 @@
namespace Kirby\Uuid;
use Kirby\Cms\App;
/**
* Base for UUIDs for models where id string
* is stored in the content, such as pages and files
@@ -107,18 +105,4 @@ abstract class ModelUuid extends Uuid
// use the most basic write method to avoid object cloning
$this->model->writeContent($data, 'default');
}
/**
* Returns permalink url
*/
public function url(): string
{
// make sure UUID is cached because the permalink
// route only looks up UUIDs from cache
if ($this->isCached() === false) {
$this->populate();
}
return App::instance()->url() . '/@/' . static::TYPE . '/' . $this->id();
}
}

View File

@@ -54,4 +54,25 @@ class PageUuid extends ModelUuid
yield from static::index($page);
}
}
/**
* Returns permalink url
*/
public function url(): string
{
// make sure UUID is cached because the permalink
// route only looks up UUIDs from cache
if ($this->isCached() === false) {
$this->populate();
}
$kirby = App::instance();
$url = $kirby->url();
if ($language = $kirby->language('current')) {
$url .= '/' . $language->code();
}
return $url . '/@/' . static::TYPE . '/' . $this->id();
}
}