Upgrade to 4.0.0

This commit is contained in:
Bastian Allgeier
2023-11-28 09:33:56 +01:00
parent f96b96af76
commit 3b0b6546ca
480 changed files with 21371 additions and 13327 deletions

View File

@@ -3,7 +3,6 @@
return [
// cms classes
'collection' => 'Kirby\Cms\Collection',
'field' => 'Kirby\Cms\Field',
'file' => 'Kirby\Cms\File',
'files' => 'Kirby\Cms\Files',
'find' => 'Kirby\Cms\Find',
@@ -24,6 +23,9 @@ return [
'users' => 'Kirby\Cms\Users',
'visitor' => 'Kirby\Cms\Visitor',
// content classes
'field' => 'Kirby\Content\Field',
// data handler
'data' => 'Kirby\Data\Data',
'json' => 'Kirby\Data\Json',
@@ -69,17 +71,25 @@ return [
'v' => 'Kirby\Toolkit\V',
'xml' => 'Kirby\Toolkit\Xml',
// TODO: remove in 4.0.0
'kirby\cms\asset' => 'Kirby\Filesystem\Asset',
'kirby\cms\dir' => 'Kirby\Filesystem\Dir',
'kirby\cms\filename' => 'Kirby\Filesystem\Filename',
'kirby\cms\filefoundation' => 'Kirby\Filesystem\IsFile',
'kirby\cms\form' => 'Kirby\Form\Form',
'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag',
'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags',
'kirby\cms\template' => 'Kirby\Template\Template',
'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir',
'kirby\toolkit\f' => 'Kirby\Filesystem\F',
'kirby\toolkit\file' => 'Kirby\Filesystem\File',
'kirby\toolkit\mime' => 'Kirby\Filesystem\Mime',
// Deprecated aliases:
// Any of these might be removed at any point in the future
'kirby\cms\asset' => 'Kirby\Filesystem\Asset',
'kirby\cms\content' => 'Kirby\Content\Content',
'kirby\cms\contenttranslation' => 'Kirby\Content\ContentTranslation',
'kirby\cms\dir' => 'Kirby\Filesystem\Dir',
'kirby\cms\filename' => 'Kirby\Filesystem\Filename',
'kirby\cms\filefoundation' => 'Kirby\Filesystem\IsFile',
'kirby\cms\field' => 'Kirby\Content\Field',
'kirby\cms\form' => 'Kirby\Form\Form',
'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag',
'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags',
'kirby\cms\template' => 'Kirby\Template\Template',
'kirby\form\options' => 'Kirby\Options\Options',
'kirby\form\optionsapi' => 'Kirby\Options\OptionsApi',
'kirby\form\optionsquery' => 'Kirby\Options\OptionsQuery',
'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir',
'kirby\toolkit\f' => 'Kirby\Filesystem\F',
'kirby\toolkit\file' => 'Kirby\Filesystem\File',
'kirby\toolkit\mime' => 'Kirby\Filesystem\Mime',
'kirby\toolkit\query' => 'Kirby\Query\Query',
];

View File

@@ -1,6 +1,6 @@
<?php
use Kirby\Exception\PermissionException;
use Kirby\Exception\AuthException;
return function () {
$auth = $this->kirby()->auth();
@@ -11,17 +11,17 @@ return function () {
$auth->type($allowImpersonation) === 'session' &&
$auth->csrf() === false
) {
throw new PermissionException('Unauthenticated');
throw new AuthException('Unauthenticated');
}
// get user from session or basic auth
if ($user = $auth->user(null, $allowImpersonation)) {
if ($user->role()->permissions()->for('access', 'panel') === false) {
throw new PermissionException(['key' => 'access.panel']);
throw new AuthException(['key' => 'access.panel']);
}
return $user;
}
throw new PermissionException('Unauthenticated');
throw new AuthException('Unauthenticated');
};

View File

@@ -3,6 +3,14 @@
/**
* Api Collection Definitions
*/
use Kirby\Cms\Files;
use Kirby\Cms\Languages;
use Kirby\Cms\Pages;
use Kirby\Cms\Roles;
use Kirby\Cms\Translations;
use Kirby\Cms\Users;
return [
/**
@@ -10,7 +18,7 @@ return [
*/
'children' => [
'model' => 'page',
'type' => 'Kirby\Cms\Pages',
'type' => Pages::class,
'view' => 'compact'
],
@@ -19,7 +27,7 @@ return [
*/
'files' => [
'model' => 'file',
'type' => 'Kirby\Cms\Files'
'type' => Files::class,
],
/**
@@ -27,7 +35,7 @@ return [
*/
'languages' => [
'model' => 'language',
'type' => 'Kirby\Cms\Languages'
'type' => Languages::class,
],
/**
@@ -35,7 +43,7 @@ return [
*/
'pages' => [
'model' => 'page',
'type' => 'Kirby\Cms\Pages',
'type' => Pages::class,
'view' => 'compact'
],
@@ -44,7 +52,7 @@ return [
*/
'roles' => [
'model' => 'role',
'type' => 'Kirby\Cms\Roles',
'type' => Roles::class,
'view' => 'compact'
],
@@ -53,7 +61,7 @@ return [
*/
'translations' => [
'model' => 'translation',
'type' => 'Kirby\Cms\Translations',
'type' => Translations::class,
'view' => 'compact'
],
@@ -63,7 +71,7 @@ return [
'users' => [
'default' => fn () => $this->users(),
'model' => 'user',
'type' => 'Kirby\Cms\Users',
'type' => Users::class,
'view' => 'compact'
]

View File

@@ -8,6 +8,7 @@ return [
'FileBlueprint' => include __DIR__ . '/models/FileBlueprint.php',
'FileVersion' => include __DIR__ . '/models/FileVersion.php',
'Language' => include __DIR__ . '/models/Language.php',
'License' => include __DIR__ . '/models/License.php',
'Page' => include __DIR__ . '/models/Page.php',
'PageBlueprint' => include __DIR__ . '/models/PageBlueprint.php',
'Role' => include __DIR__ . '/models/Role.php',

View File

@@ -59,7 +59,7 @@ return [
'url' => fn (File $file) => $file->url(),
'uuid' => fn (File $file) => $file->uuid()?->toString()
],
'type' => 'Kirby\Cms\File',
'type' => File::class,
'views' => [
'default' => [
'content',

View File

@@ -12,7 +12,6 @@ return [
'tabs' => fn (FileBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (FileBlueprint $blueprint) => $blueprint->title(),
],
'type' => 'Kirby\Cms\FileBlueprint',
'views' => [
],
'type' => FileBlueprint::class,
'views' => [],
];

View File

@@ -20,7 +20,7 @@ return [
'type' => fn (FileVersion $file) => $file->type(),
'url' => fn (FileVersion $file) => $file->url(),
],
'type' => 'Kirby\Cms\FileVersion',
'type' => FileVersion::class,
'views' => [
'default' => [
'dimensions',

View File

@@ -15,7 +15,7 @@ return [
'rules' => fn (Language $language) => $language->rules(),
'url' => fn (Language $language) => $language->url(),
],
'type' => 'Kirby\Cms\Language',
'type' => Language::class,
'views' => [
'default' => [
'code',

View File

@@ -0,0 +1,17 @@
<?php
use Kirby\Cms\License;
/**
* Page
*/
return [
'fields' => [
'status' => fn (License $license) => $license->status()->value(),
'code' => function (License $license) {
return $this->kirby()->user()->isAdmin() ? $license->code() : $license->code(true);
},
'type' => fn (License $license) => $license->type()->label(),
],
'type' => License::class,
];

View File

@@ -40,7 +40,7 @@ return [
'url' => fn (Page $page) => $page->url(),
'uuid' => fn (Page $page) => $page->uuid()?->toString()
],
'type' => 'Kirby\Cms\Page',
'type' => Page::class,
'views' => [
'compact' => [
'id',

View File

@@ -15,7 +15,6 @@ return [
'tabs' => fn (PageBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (PageBlueprint $blueprint) => $blueprint->title(),
],
'type' => 'Kirby\Cms\PageBlueprint',
'views' => [
],
'type' => PageBlueprint::class,
'views' => [],
];

View File

@@ -12,7 +12,7 @@ return [
'permissions' => fn (Role $role) => $role->permissions()->toArray(),
'title' => fn (Role $role) => $role->title(),
],
'type' => 'Kirby\Cms\Role',
'type' => Role::class,
'views' => [
'compact' => [
'description',

View File

@@ -19,7 +19,7 @@ return [
'title' => fn (Site $site) => $site->title()->value(),
'url' => fn (Site $site) => $site->url(),
],
'type' => 'Kirby\Cms\Site',
'type' => Site::class,
'views' => [
'compact' => [
'title',

View File

@@ -12,6 +12,6 @@ return [
'tabs' => fn (SiteBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (SiteBlueprint $blueprint) => $blueprint->title(),
],
'type' => 'Kirby\Cms\SiteBlueprint',
'type' => SiteBlueprint::class,
'views' => [],
];

View File

@@ -49,7 +49,7 @@ return [
return null;
}
],
'type' => 'Kirby\Cms\System',
'type' => System::class,
'views' => [
'login' => [
'authStatus',

View File

@@ -13,7 +13,7 @@ return [
'id' => fn (Translation $translation) => $translation->id(),
'name' => fn (Translation $translation) => $translation->name(),
],
'type' => 'Kirby\Cms\Translation',
'type' => Translation::class,
'views' => [
'compact' => [
'direction',

View File

@@ -27,7 +27,7 @@ return [
'username' => fn (User $user) => $user->username(),
'uuid' => fn (User $user) => $user->uuid()?->toString()
],
'type' => 'Kirby\Cms\User',
'type' => User::class,
'views' => [
'default' => [
'avatar',

View File

@@ -12,7 +12,6 @@ return [
'tabs' => fn (UserBlueprint $blueprint) => $blueprint->tabs(),
'title' => fn (UserBlueprint $blueprint) => $blueprint->title(),
],
'type' => 'Kirby\Cms\UserBlueprint',
'views' => [
],
'type' => UserBlueprint::class,
'views' => [],
];

View File

@@ -19,7 +19,10 @@ return function ($kirby) {
// only add the language routes if the
// multi language setup is activated
if ($kirby->option('languages', false) !== false) {
$routes = array_merge($routes, include __DIR__ . '/routes/languages.php');
$routes = array_merge(
$routes,
include __DIR__ . '/routes/languages.php'
);
}
return $routes;

View File

@@ -1,7 +1,8 @@
<?php
// routing pattern to match all models with files
$pattern = '(account|pages/[^/]+|site|users/[^/]+)';
$filePattern = '(account/|pages/[^/]+/|site/|users/[^/]+/|)files/(:any)';
$parentPattern = '(account|pages/[^/]+|site|users/[^/]+)/files';
/**
* Files Routes
@@ -9,14 +10,14 @@ $pattern = '(account|pages/[^/]+|site|users/[^/]+)';
return [
[
'pattern' => $pattern . '/files/(:any)/sections/(:any)',
'pattern' => $filePattern . '/sections/(:any)',
'method' => 'GET',
'action' => function (string $path, string $filename, string $sectionName) {
return $this->file($path, $filename)->blueprint()->section($sectionName)?->toResponse();
}
],
[
'pattern' => $pattern . '/files/(:any)/fields/(:any)/(:all?)',
'pattern' => $filePattern . '/fields/(:any)/(:all?)',
'method' => 'ALL',
'action' => function (string $parent, string $filename, string $fieldName, string $path = null) {
if ($file = $this->file($parent, $filename)) {
@@ -25,14 +26,14 @@ return [
}
],
[
'pattern' => $pattern . '/files',
'pattern' => $parentPattern,
'method' => 'GET',
'action' => function (string $path) {
return $this->parent($path)->files()->sorted();
return $this->files($path)->sorted();
}
],
[
'pattern' => $pattern . '/files',
'pattern' => $parentPattern,
'method' => 'POST',
'action' => function (string $path) {
// move_uploaded_file() not working with unit test
@@ -54,10 +55,10 @@ return [
}
],
[
'pattern' => $pattern . '/files/search',
'pattern' => $parentPattern . '/search',
'method' => 'GET|POST',
'action' => function (string $path) {
$files = $this->parent($path)->files();
$files = $this->files($path);
if ($this->requestMethod() === 'GET') {
return $files->search($this->requestQuery('q'));
@@ -67,24 +68,24 @@ return [
}
],
[
'pattern' => $pattern . '/files/sort',
'pattern' => $parentPattern . '/sort',
'method' => 'PATCH',
'action' => function (string $path) {
return $this->parent($path)->files()->changeSort(
return $this->files($path)->changeSort(
$this->requestBody('files'),
$this->requestBody('index')
);
}
],
[
'pattern' => $pattern . '/files/(:any)',
'pattern' => $filePattern,
'method' => 'GET',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename);
}
],
[
'pattern' => $pattern . '/files/(:any)',
'pattern' => $filePattern,
'method' => 'PATCH',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->update(
@@ -95,7 +96,7 @@ return [
}
],
[
'pattern' => $pattern . '/files/(:any)',
'pattern' => $filePattern,
'method' => 'POST',
'action' => function (string $path, string $filename) {
// move the source file from the temp dir
@@ -105,28 +106,29 @@ return [
}
],
[
'pattern' => $pattern . '/files/(:any)',
'pattern' => $filePattern,
'method' => 'DELETE',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->delete();
}
],
[
'pattern' => $pattern . '/files/(:any)/name',
'pattern' => $filePattern . '/name',
'method' => 'PATCH',
'action' => function (string $path, string $filename) {
return $this->file($path, $filename)->changeName($this->requestBody('name'));
}
],
[
'pattern' => 'files/search',
'pattern' => $parentPattern . '/search',
'method' => 'GET|POST',
'action' => function () {
$files = $this
->site()
->index(true)
->filter('isReadable', true)
->files();
->filter('isListable', true)
->files()
->filter('isListable', true);
if ($this->requestMethod() === 'GET') {
return $files->search($this->requestQuery('q'));

View File

@@ -0,0 +1,35 @@
<?php
// @codeCoverageIgnoreStart
return [
'routes' => function ($kirby) {
return [
[
'pattern' => 'query',
'method' => 'POST|GET',
'auth' => $kirby->option('kql.auth') !== false,
'action' => function () use ($kirby) {
$kql = '\Kirby\Kql\Kql';
if (class_exists($kql) === false) {
return [
'code' => 500,
'status' => 'error',
'message' => 'KQL plugin is not installed',
];
}
$input = $kirby->request()->get();
$result = $kql::run($input);
return [
'code' => 200,
'result' => $result,
'status' => 'ok',
];
}
]
];
}
];
// @codeCoverageIgnoreEnd

View File

@@ -4,8 +4,9 @@
/**
* Page Routes
*/
return [
return [
[
'pattern' => 'pages/(:any)',
'method' => 'GET',

View File

@@ -75,7 +75,7 @@ return [
$pages = $this
->site()
->index(true)
->filter('isReadable', true);
->filter('isListable', true);
if ($this->requestMethod() === 'GET') {
return $pages->search($this->requestQuery('q'));

View File

@@ -8,6 +8,7 @@ return function () {
'label' => I18n::translate('view.account'),
'search' => 'users',
'dialogs' => require __DIR__ . '/account/dialogs.php',
'drawers' => require __DIR__ . '/account/drawers.php',
'dropdowns' => require __DIR__ . '/account/dropdowns.php',
'views' => require __DIR__ . '/account/views.php'
];

View File

@@ -1,5 +1,7 @@
<?php
use Kirby\Panel\UserTotpEnableDialog;
$dialogs = require __DIR__ . '/../users/dialogs.php';
return [
@@ -46,6 +48,13 @@ return [
'submit' => $dialogs['user.delete']['submit'],
],
// account fields dialogs
'account.fields' => [
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
'load' => $dialogs['user.fields']['load'],
'submit' => $dialogs['user.fields']['submit']
],
// change file name
'account.file.changeName' => [
'pattern' => '(account)/files/(:any)/changeName',
@@ -60,6 +69,13 @@ return [
'submit' => $dialogs['user.file.changeSort']['submit'],
],
// change file template
'account.file.changeTemplate' => [
'pattern' => '(account)/files/(:any)/changeTemplate',
'load' => $dialogs['user.file.changeTemplate']['load'],
'submit' => $dialogs['user.file.changeTemplate']['submit'],
],
// delete
'account.file.delete' => [
'pattern' => '(account)/files/(:any)/delete',
@@ -67,4 +83,24 @@ return [
'submit' => $dialogs['user.file.delete']['submit'],
],
// account file fields dialogs
'account.file.fields' => [
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
'load' => $dialogs['user.file.fields']['load'],
'submit' => $dialogs['user.file.fields']['submit']
],
// account enable TOTP
'account.totp.enable' => [
'pattern' => '(account)/totp/enable',
'load' => fn () => (new UserTotpEnableDialog())->load(),
'submit' => fn () => (new UserTotpEnableDialog())->submit()
],
// account disable TOTP
'account.totp.disable' => [
'pattern' => '(account)/totp/disable',
'load' => $dialogs['user.totp.disable']['load'],
'submit' => $dialogs['user.totp.disable']['submit']
],
];

View File

@@ -0,0 +1,19 @@
<?php
$drawers = require __DIR__ . '/../users/drawers.php';
return [
// account fields drawers
'account.fields' => [
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
'load' => $drawers['user.fields']['load'],
'submit' => $drawers['user.fields']['submit']
],
// account file fields drawers
'account.file.fields' => [
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
'load' => $drawers['user.file.fields']['load'],
'submit' => $drawers['user.file.fields']['submit']
],
];

View File

@@ -2,6 +2,7 @@
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Toolkit\I18n;
return [
'account' => [
@@ -19,6 +20,13 @@ return [
],
'account.password' => [
'pattern' => 'reset-password',
'action' => fn () => ['component' => 'k-reset-password-view']
'action' => fn () => [
'component' => 'k-reset-password-view',
'breadcrumb' => [
[
'label' => I18n::translate('view.resetPassword')
]
]
]
]
];

View File

@@ -0,0 +1,61 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Field;
return [
'model' => [
'load' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
'file' => [
'load' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::dialog(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
];

View File

@@ -0,0 +1,61 @@
<?php
use Kirby\Cms\Find;
use Kirby\Panel\Field;
return [
'model' => [
'load' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::parent($modelPath),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
'file' => [
'load' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'GET'
);
},
'submit' => function (
string $modelPath,
string $filename,
string $fieldName,
string|null $path = null
) {
return Field::drawer(
model: Find::file($modelPath, $filename),
fieldName: $fieldName,
path: $path,
method: 'POST'
);
}
],
];

View File

@@ -40,7 +40,8 @@ return [
},
'submit' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$renamed = $file->changeName($file->kirby()->request()->get('name'));
$name = $file->kirby()->request()->get('name');
$renamed = $file->changeName($name);
$oldUrl = $file->panel()->url(true);
$newUrl = $renamed->panel()->url(true);
$response = [
@@ -96,6 +97,44 @@ return [
}
],
'changeTemplate' => [
'load' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$blueprints = $file->blueprints();
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'warning' => [
'type' => 'info',
'theme' => 'notice',
'text' => I18n::translate('file.changeTemplate.notice')
],
'template' => Field::template($blueprints, [
'required' => true
])
],
'theme' => 'notice',
'submitButton' => I18n::translate('change'),
'value' => [
'template' => $file->template()
]
]
];
},
'submit' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
$template = $file->kirby()->request()->get('template');
$file->changeTemplate($template);
return [
'event' => 'file.changeTemplate',
];
}
],
'delete' => [
'load' => function (string $path, string $filename) {
$file = Find::file($path, $filename);
@@ -129,4 +168,5 @@ return [
];
}
],
];

View File

@@ -0,0 +1,11 @@
<?php
return function () {
return [
'icon' => 'lab',
'label' => 'Lab',
'menu' => false,
'drawers' => require __DIR__ . '/lab/drawers.php',
'views' => require __DIR__ . '/lab/views.php'
];
};

View File

@@ -0,0 +1,30 @@
<?php
use Kirby\Panel\Lab\Docs;
return [
'lab.docs' => [
'pattern' => 'lab/docs/(:any)',
'load' => function (string $component) {
if (Docs::installed() === false) {
return [
'component' => 'k-text-drawer',
'props' => [
'text' => 'The UI docs are not installed.'
]
];
}
$docs = new Docs($component);
return [
'component' => 'k-lab-docs-drawer',
'props' => [
'icon' => 'book',
'title' => $component,
'docs' => $docs->toArray()
]
];
},
],
];

View File

@@ -0,0 +1,138 @@
<?php
use Kirby\Panel\Lab\Category;
use Kirby\Panel\Lab\Docs;
return [
'lab' => [
'pattern' => 'lab',
'action' => function () {
return [
'component' => 'k-lab-index-view',
'props' => [
'categories' => Category::all(),
'info' => Category::installed() ? null : 'The default Lab examples are not installed.',
'tab' => 'examples',
],
];
}
],
'lab.docs' => [
'pattern' => 'lab/docs',
'action' => function () {
$props = match (Docs::installed()) {
true => [
'categories' => [['examples' => Docs::all()]],
'tab' => 'docs',
],
false => [
'info' => 'The UI docs are not installed.',
'tab' => 'docs',
]
};
return [
'component' => 'k-lab-index-view',
'title' => 'Docs',
'breadcrumb' => [
[
'label' => 'Docs',
'link' => 'lab/docs'
]
],
'props' => $props,
];
}
],
'lab.doc' => [
'pattern' => 'lab/docs/(:any)',
'action' => function (string $component) {
$crumbs = [
[
'label' => 'Docs',
'link' => 'lab/docs'
],
[
'label' => $component,
'link' => 'lab/docs/' . $component
]
];
if (Docs::installed() === false) {
return [
'component' => 'k-lab-index-view',
'title' => $component,
'breadcrumb' => $crumbs,
'props' => [
'info' => 'The UI docs are not installed.',
'tab' => 'docs',
],
];
}
$docs = new Docs($component);
return [
'component' => 'k-lab-docs-view',
'title' => $component,
'breadcrumb' => $crumbs,
'props' => [
'component' => $component,
'docs' => $docs->toArray(),
'lab' => $docs->lab()
]
];
}
],
'lab.vue' => [
'pattern' => [
'lab/(:any)/(:any)/index.vue',
'lab/(:any)/(:any)/(:any)/index.vue'
],
'action' => function (
string $category,
string $id,
string|null $tab = null
) {
return Category::factory($category)->example($id, $tab)->serve();
}
],
'lab.example' => [
'pattern' => 'lab/(:any)/(:any)/(:any?)',
'action' => function (
string $category,
string $id,
string|null $tab = null
) {
$category = Category::factory($category);
$example = $category->example($id, $tab);
$props = $example->props();
$vue = $example->vue();
return [
'component' => 'k-lab-playground-view',
'breadcrumb' => [
[
'label' => $category->name(),
],
[
'label' => $example->title(),
'link' => $example->url()
]
],
'props' => [
'docs' => $props['docs'] ?? null,
'examples' => $vue['examples'],
'file' => $example->module(),
'github' => $example->github(),
'props' => $props,
'styles' => $vue['style'],
'tab' => $example->tab(),
'tabs' => array_values($example->tabs()),
'template' => $vue['template'],
'title' => $example->title(),
],
];
}
]
];

View File

@@ -4,7 +4,7 @@ use Kirby\Toolkit\I18n;
return function ($kirby) {
return [
'icon' => 'globe',
'icon' => 'translate',
'label' => I18n::translate('view.languages'),
'menu' => true,
'dialogs' => require __DIR__ . '/languages/dialogs.php',

View File

@@ -2,13 +2,15 @@
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Panel\Field;
use Kirby\Cms\LanguageVariable;
use Kirby\Exception\NotFoundException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
$languageDialogFields = [
'name' => [
'counter' => false,
'label' => I18n::translate('language.name'),
'type' => 'text',
'required' => true,
@@ -19,7 +21,7 @@ $languageDialogFields = [
'type' => 'text',
'required' => true,
'counter' => false,
'icon' => 'globe',
'icon' => 'translate',
'width' => '1/2'
],
'direction' => [
@@ -34,11 +36,27 @@ $languageDialogFields = [
'width' => '1/2'
],
'locale' => [
'label' => I18n::translate('language.locale'),
'type' => 'text',
'counter' => false,
'label' => I18n::translate('language.locale'),
'type' => 'text',
],
];
$translationDialogFields = [
'key' => [
'counter' => false,
'icon' => null,
'label' => I18n::translate('language.variable.key'),
'type' => 'text'
],
'value' => [
'buttons' => false,
'counter' => false,
'label' => I18n::translate('language.variable.value'),
'type' => 'textarea'
]
];
return [
// create language
@@ -92,8 +110,10 @@ return [
},
'submit' => function (string $id) {
Find::language($id)->delete();
return [
'event' => 'language.delete',
'event' => 'language.delete',
'redirect' => 'languages'
];
}
],
@@ -152,4 +172,95 @@ return [
];
}
],
'language.translation.create' => [
'pattern' => 'languages/(:any)/translations/create',
'load' => function (string $languageCode) use ($translationDialogFields) {
// find the language to make sure it exists
Find::language($languageCode);
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $translationDialogFields,
'size' => 'large',
],
];
},
'submit' => function (string $languageCode) {
$request = App::instance()->request();
$language = Find::language($languageCode);
$key = $request->get('key', '');
$value = $request->get('value', '');
LanguageVariable::create($key, $value);
if ($language->isDefault() === false) {
$language->variable($key)->update($value);
}
return true;
}
],
'language.translation.delete' => [
'pattern' => 'languages/(:any)/translations/(:any)/delete',
'load' => function (string $languageCode, string $translationKey) {
$variable = Find::language($languageCode)->variable($translationKey, true);
if ($variable->exists() === false) {
throw new NotFoundException([
'key' => 'language.variable.notFound'
]);
}
return [
'component' => 'k-remove-dialog',
'props' => [
'text' => I18n::template('language.variable.delete.confirm', [
'key' => Escape::html($variable->key())
])
],
];
},
'submit' => function (string $languageCode, string $translationKey) {
return Find::language($languageCode)->variable($translationKey, true)->delete();
}
],
'language.translation.update' => [
'pattern' => 'languages/(:any)/translations/(:any)/update',
'load' => function (string $languageCode, string $translationKey) use ($translationDialogFields) {
$variable = Find::language($languageCode)->variable($translationKey, true);
if ($variable->exists() === false) {
throw new NotFoundException([
'key' => 'language.variable.notFound'
]);
}
$fields = $translationDialogFields;
$fields['key']['disabled'] = true;
$fields['value']['autofocus'] = true;
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $fields,
'size' => 'large',
'value' => [
'key' => $variable->key(),
'value' => $variable->value()
]
],
];
},
'submit' => function (string $languageCode, string $translationKey) {
Find::language($languageCode)->variable($translationKey, true)->update(
App::instance()->request()->get('value')
);
return true;
}
]
];

View File

@@ -1,9 +1,104 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
return [
'language' => [
'pattern' => 'languages/(:any)',
'when' => function (): bool {
return App::instance()->option('languages.variables', true) !== false;
},
'action' => function (string $code) {
$language = Find::language($code);
$link = '/languages/' . $language->code();
$strings = [];
$foundation = App::instance()->defaultLanguage()->translations();
$translations = $language->translations();
ksort($foundation);
foreach ($foundation as $key => $value) {
$strings[] = [
'key' => $key,
'value' => $translations[$key] ?? null,
'options' => [
[
'click' => 'update',
'icon' => 'edit',
'text' => I18n::translate('edit'),
],
[
'click' => 'delete',
'disabled' => $language->isDefault() === false,
'icon' => 'trash',
'text' => I18n::translate('delete'),
]
]
];
}
$next = function () use ($language) {
if ($next = $language->next()) {
return [
'link' => '/languages/' . $next->code(),
'title' => $next->name(),
];
}
};
$prev = function () use ($language) {
if ($prev = $language->prev()) {
return [
'link' => '/languages/' . $prev->code(),
'title' => $prev->name(),
];
}
};
return [
'component' => 'k-language-view',
'breadcrumb' => [
[
'label' => $name = $language->name(),
'link' => $link,
]
],
'props' => [
'deletable' => $language->isDeletable(),
'code' => Escape::html($language->code()),
'default' => $language->isDefault(),
'direction' => $language->direction(),
'id' => $language->code(),
'info' => [
[
'label' => 'Status',
'value' => I18n::translate('language.' . ($language->isDefault() ? 'default' : 'secondary')),
],
[
'label' => I18n::translate('language.code'),
'value' => $language->code(),
],
[
'label' => I18n::translate('language.locale'),
'value' => $language->locale(LC_ALL)
],
[
'label' => I18n::translate('language.direction'),
'value' => I18n::translate('language.direction.' . $language->direction()),
],
],
'name' => $name,
'next' => $next,
'prev' => $prev,
'translations' => $strings,
'url' => $language->url(),
]
];
}
],
'languages' => [
'pattern' => 'languages',
'action' => function () {
@@ -13,13 +108,15 @@ return [
'component' => 'k-languages-view',
'props' => [
'languages' => $kirby->languages()->values(fn ($language) => [
'default' => $language->isDefault(),
'id' => $language->code(),
'info' => Escape::html($language->code()),
'text' => Escape::html($language->name()),
])
'deletable' => $language->isDeletable(),
'default' => $language->isDefault(),
'id' => $language->code(),
'info' => Escape::html($language->code()),
'text' => Escape::html($language->name()),
]),
'variables' => $kirby->option('languages.variables', true)
]
];
}
],
]
];

View File

@@ -0,0 +1,11 @@
<?php
use Kirby\Toolkit\I18n;
return function () {
return [
'icon' => 'search',
'label' => I18n::translate('search'),
'views' => require __DIR__ . '/search/views.php'
];
};

View File

@@ -0,0 +1,17 @@
<?php
use Kirby\Cms\App;
return [
'search' => [
'pattern' => 'search',
'action' => function () {
return [
'component' => 'k-search-view',
'props' => [
'type' => App::instance()->request()->get('type'),
]
];
}
],
];

View File

@@ -11,7 +11,9 @@ return function ($kirby) {
'label' => $kirby->site()->blueprint()->title() ?? I18n::translate('view.site'),
'menu' => true,
'dialogs' => require __DIR__ . '/site/dialogs.php',
'drawers' => require __DIR__ . '/site/drawers.php',
'dropdowns' => require __DIR__ . '/site/dropdowns.php',
'requests' => require __DIR__ . '/site/requests.php',
'searches' => require __DIR__ . '/site/searches.php',
'views' => require __DIR__ . '/site/views.php',
];

View File

@@ -2,14 +2,19 @@
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Cms\PageRules;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\PermissionException;
use Kirby\Panel\ChangesDialog;
use Kirby\Panel\Field;
use Kirby\Panel\PageCreateDialog;
use Kirby\Panel\Panel;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuids;
$fields = require __DIR__ . '/../fields/dialogs.php';
$files = require __DIR__ . '/../files/dialogs.php';
return [
@@ -155,10 +160,16 @@ return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'notice' => [
'type' => 'info',
'theme' => 'notice',
'text' => I18n::translate('page.changeTemplate.notice')
],
'template' => Field::template($blueprints, [
'required' => true
])
],
'theme' => 'notice',
'submitButton' => I18n::translate('change'),
'value' => [
'template' => $page->intendedTemplate()->name()
@@ -167,9 +178,10 @@ return [
];
},
'submit' => function (string $id) {
$request = App::instance()->request();
$page = Find::page($id);
$template = App::instance()->request()->get('template');
Find::page($id)->changeTemplate($request->get('template'));
$page->changeTemplate($template);
return [
'event' => 'page.changeTemplate',
@@ -224,17 +236,8 @@ return [
$slug = trim($request->get('slug', ''));
// basic input validation before we move on
if (Str::length($title) === 0) {
throw new InvalidArgumentException([
'key' => 'page.changeTitle.empty'
]);
}
if (Str::length($slug) === 0) {
throw new InvalidArgumentException([
'key' => 'page.slug.invalid'
]);
}
PageRules::validateTitleLength($title);
PageRules::validateSlugLength($slug);
// nothing changed
if ($page->title()->value() === $title && $page->slug() === $slug) {
@@ -277,93 +280,30 @@ return [
'page.create' => [
'pattern' => 'pages/create',
'load' => function () {
$kirby = App::instance();
$request = $kirby->request();
$request = App::instance()->request();
$dialog = new PageCreateDialog(
parentId: $request->get('parent'),
sectionId: $request->get('section'),
slug: $request->get('slug'),
template: $request->get('template'),
title: $request->get('title'),
viewId: $request->get('view'),
);
// the parent model for the new page
$parent = $request->get('parent', 'site');
// the view on which the add button is located
// this is important to find the right section
// and provide the correct templates for the new page
$view = $request->get('view', $parent);
// templates will be fetched depending on the
// section settings in the blueprint
$section = $request->get('section');
// this is the parent model
$model = Find::parent($parent);
// this is the view model
// i.e. site if the add button is on
// the dashboard
$view = Find::parent($view);
// available blueprints/templates for the new page
// are always loaded depending on the matching section
// in the view model blueprint
$blueprints = $view->blueprints($section);
// the pre-selected template
$template = $blueprints[0]['name'] ?? $blueprints[0]['value'] ?? null;
$fields = [
'parent' => Field::hidden(),
'title' => Field::title([
'required' => true,
'preselect' => true
]),
'slug' => Field::slug([
'required' => true,
'sync' => 'title',
'path' => empty($model->id()) === false ? '/' . $model->id() . '/' : '/'
]),
'template' => Field::hidden()
];
// only show template field if > 1 templates available
// or when in debug mode
if (count($blueprints) > 1 || $kirby->option('debug') === true) {
$fields['template'] = Field::template($blueprints, [
'required' => true
]);
}
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => $fields,
'submitButton' => I18n::translate('page.draft.create'),
'value' => [
'parent' => $parent,
'slug' => '',
'template' => $template,
'title' => '',
]
]
];
return $dialog->load();
},
'submit' => function () {
$request = App::instance()->request();
$title = trim($request->get('title', ''));
$dialog = new PageCreateDialog(
parentId: $request->get('parent'),
sectionId: $request->get('section'),
slug: $request->get('slug'),
template: $request->get('template'),
title: $request->get('title'),
viewId: $request->get('view'),
);
if (Str::length($title) === 0) {
throw new InvalidArgumentException([
'key' => 'page.changeTitle.empty'
]);
}
$page = Find::parent($request->get('parent', 'site'))->createChild([
'content' => ['title' => $title],
'slug' => $request->get('slug'),
'template' => $request->get('template'),
]);
return [
'event' => 'page.create',
'redirect' => $page->panel()->url(true)
];
return $dialog->submit($request->get());
}
],
@@ -529,6 +469,13 @@ return [
}
],
// page field dialogs
'page.fields' => [
'pattern' => '(pages/.*?)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit']
],
// change filename
'page.file.changeName' => [
'pattern' => '(pages/.*?)/files/(:any)/changeName',
@@ -543,6 +490,13 @@ return [
'submit' => $files['changeSort']['submit'],
],
// change template
'page.file.changeTemplate' => [
'pattern' => '(pages/.*?)/files/(:any)/changeTemplate',
'load' => $files['changeTemplate']['load'],
'submit' => $files['changeTemplate']['submit'],
],
// delete
'page.file.delete' => [
'pattern' => '(pages/.*?)/files/(:any)/delete',
@@ -550,6 +504,56 @@ return [
'submit' => $files['delete']['submit'],
],
// page file field dialogs
'page.file.fields' => [
'pattern' => '(pages/.*?)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit'],
],
// move page
'page.move' => [
'pattern' => 'pages/(:any)/move',
'load' => function (string $id) {
$page = Find::page($id);
$parent = $page->parentModel();
if (Uuids::enabled() === false) {
$parentId = $parent?->id() ?? '/';
} else {
$parentId = $parent?->uuid()->toString() ?? 'site://';
}
return [
'component' => 'k-page-move-dialog',
'props' => [
'value' => [
'move' => $page->panel()->url(true),
'parent' => $parentId
]
]
];
},
'submit' => function (string $id) {
$kirby = App::instance();
$parentId = $kirby->request()->get('parent');
$parent = (empty($parentId) === true || $parentId === '/' || $parentId === 'site://') ? $kirby->site() : Find::page($parentId);
$oldPage = Find::page($id);
$newPage = $oldPage->move($parent);
return [
'event' => 'page.move',
'redirect' => $newPage->panel()->url(true),
'dispatch' => [
'content/move' => [
$oldPage->panel()->url(true),
$newPage->panel()->url(true)
]
],
];
}
],
// change site title
'site.changeTitle' => [
'pattern' => 'site/changeTitle',
@@ -572,14 +576,21 @@ return [
},
'submit' => function () {
$kirby = App::instance();
$kirby->site()->changeTitle($kirby->request()->get('title'));
return [
'event' => 'site.changeTitle',
];
}
],
// site field dialogs
'site.fields' => [
'pattern' => '(site)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit'],
],
// change filename
'site.file.changeName' => [
'pattern' => '(site)/files/(:any)/changeName',
@@ -594,6 +605,13 @@ return [
'submit' => $files['changeSort']['submit'],
],
// change template
'site.file.changeTemplate' => [
'pattern' => '(site)/files/(:any)/changeTemplate',
'load' => $files['changeTemplate']['load'],
'submit' => $files['changeTemplate']['submit'],
],
// delete
'site.file.delete' => [
'pattern' => '(site)/files/(:any)/delete',
@@ -601,4 +619,24 @@ return [
'submit' => $files['delete']['submit'],
],
// site file field dialogs
'site.file.fields' => [
'pattern' => '(site)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit'],
],
// content changes
'changes' => [
'pattern' => 'changes',
'load' => function () {
$dialog = new ChangesDialog();
return $dialog->load();
},
'submit' => function () {
$dialog = new ChangesDialog();
$ids = App::instance()->request()->get('ids');
return $dialog->submit($ids);
}
],
];

View File

@@ -0,0 +1,33 @@
<?php
$fields = require __DIR__ . '/../fields/drawers.php';
return [
// page field drawers
'page.fields' => [
'pattern' => '(pages/.*?)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit']
],
// page file field drawers
'page.file.fields' => [
'pattern' => '(pages/.*?)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit'],
],
// site field drawers
'site.fields' => [
'pattern' => '(site)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit'],
],
// site file field drawers
'site.file.fields' => [
'pattern' => '(site)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit'],
],
];

View File

@@ -1,14 +1,9 @@
<?php
use Kirby\Panel\Dropdown;
$files = require __DIR__ . '/../files/dropdowns.php';
return [
'changes' => [
'pattern' => 'changes',
'options' => fn () => Dropdown::changes()
],
'page' => [
'pattern' => 'pages/(:any)',
'options' => function (string $path) {

View File

@@ -0,0 +1,66 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\Find;
use Kirby\Toolkit\I18n;
return [
'tree' => [
'pattern' => 'site/tree',
'action' => function () {
$kirby = App::instance();
$request = $kirby->request();
$move = $request->get('move');
$move = $move ? Find::parent($move) : null;
$parent = $request->get('parent');
if ($parent === null) {
$site = $kirby->site();
$panel = $site->panel();
$uuid = $site->uuid()?->toString();
$url = $site->url();
$value = $uuid ?? '/';
return [
[
'children' => $panel->url(true),
'disabled' => $move?->isMovableTo($site) === false,
'hasChildren' => true,
'icon' => 'home',
'id' => '/',
'label' => I18n::translate('view.site'),
'open' => false,
'url' => $url,
'uuid' => $uuid,
'value' => $value
]
];
}
$parent = Find::parent($parent);
$pages = [];
foreach ($parent->childrenAndDrafts()->filterBy('isListable', true) as $child) {
$panel = $child->panel();
$uuid = $child->uuid()?->toString();
$url = $child->url();
$value = $uuid ?? $child->id();
$pages[] = [
'children' => $panel->url(true),
'disabled' => $move?->isMovableTo($child) === false,
'hasChildren' => $child->hasChildren() === true || $child->hasDrafts() === true,
'icon' => $panel->image()['icon'] ?? null,
'id' => $child->id(),
'open' => false,
'label' => $child->title()->value(),
'url' => $url,
'uuid' => $uuid,
'value' => $value
];
}
return $pages;
}
]
];

View File

@@ -8,50 +8,49 @@ return [
'pages' => [
'label' => I18n::translate('pages'),
'icon' => 'page',
'query' => function (string $query = null) {
$pages = App::instance()->site()
'query' => function (string $query = null, int $limit, int $page) {
$kirby = App::instance();
$pages = $kirby->site()
->index(true)
->search($query)
->filter('isReadable', true)
->limit(10);
->filter('isListable', true)
->paginate($limit, $page);
$results = [];
foreach ($pages as $page) {
$results[] = [
return [
'results' => $pages->values(fn ($page) => [
'image' => $page->panel()->image(),
'text' => Escape::html($page->title()->value()),
'link' => $page->panel()->url(true),
'info' => Escape::html($page->id())
];
}
return $results;
'info' => Escape::html($page->id()),
'uuid' => $page->uuid()->toString(),
]),
'pagination' => $pages->pagination()->toArray()
];
}
],
'files' => [
'label' => I18n::translate('files'),
'icon' => 'image',
'query' => function (string $query = null) {
$files = App::instance()->site()
'query' => function (string $query = null, int $limit, int $page) {
$kirby = App::instance();
$files = $kirby->site()
->index(true)
->filter('isReadable', true)
->filter('isListable', true)
->files()
->filter('isListable', true)
->search($query)
->limit(10);
->paginate($limit, $page);
$results = [];
foreach ($files as $file) {
$results[] = [
return [
'results' => $files->values(fn ($file) => [
'image' => $file->panel()->image(),
'text' => Escape::html($file->filename()),
'link' => $file->panel()->url(true),
'info' => Escape::html($file->id())
];
}
return $results;
'info' => Escape::html($file->id()),
'uuid' => $file->uuid()->toString(),
]),
'pagination' => $files->pagination()->toArray()
];
}
]
];

View File

@@ -1,6 +1,7 @@
<?php
use Kirby\Cms\App;
use Kirby\Exception\LogicException;
use Kirby\Panel\Field;
use Kirby\Toolkit\I18n;
@@ -8,64 +9,85 @@ return [
// license key
'license' => [
'load' => function () {
$license = App::instance()->system()->license();
// @codeCoverageIgnoreStart
// the system is registered but the license
// key is only visible for admins
if ($license === true) {
$license = 'Kirby 3';
}
// @codeCoverageIgnoreEnd
$kirby = App::instance();
$license = $kirby->system()->license();
$obfuscated = $kirby->user()->isAdmin() === false;
$status = $license->status();
$renewable = $status->renewable();
return [
'component' => 'k-form-dialog',
'component' => 'k-license-dialog',
'props' => [
'size' => 'medium',
'fields' => [
'license' => [
'type' => 'info',
'label' => I18n::translate('license'),
'text' => $license ? $license : I18n::translate('license.unregistered.label'),
'theme' => $license ? 'code' : 'negative',
'help' => $license ?
// @codeCoverageIgnoreStart
'<a href="https://hub.getkirby.com">' . I18n::translate('license.manage') . ' &rarr;</a>' :
// @codeCoverageIgnoreEnd
'<a href="https://getkirby.com/buy">' . I18n::translate('license.buy') . ' &rarr;</a>'
]
'license' => [
'code' => $license->code($obfuscated),
'icon' => $status->icon(),
'info' => $status->info($license->renewal('Y-m-d')),
'theme' => $status->theme(),
'type' => $license->label(),
],
'submitButton' => false,
'cancelButton' => false,
'cancelButton' => $renewable,
'submitButton' => $renewable ? [
'icon' => 'refresh',
'text' => I18n::translate('renew'),
'theme' => 'love',
] : false,
]
];
},
'submit' => function () {
// @codeCoverageIgnoreStart
$response = App::instance()->system()->license()->upgrade();
// the upgrade is still needed
if ($response['status'] === 'upgrade') {
return [
'redirect' => $response['url']
];
}
// the upgrade has already been completed
if ($response['status'] === 'complete') {
return [
'event' => 'system.renew',
'message' => I18n::translate('license.success')
];
}
throw new LogicException('The upgrade failed');
// @codeCoverageIgnoreEnd
}
],
// license registration
'registration' => [
'load' => function () {
$system = App::instance()->system();
$local = $system->isLocal();
return [
'component' => 'k-form-dialog',
'props' => [
'fields' => [
'domain' => [
'label' => I18n::translate('license.activate.label'),
'type' => 'info',
'theme' => $system->isLocal() ? 'notice' : 'info',
'text' => I18n::template('license.register.' . ($system->isLocal() ? 'local' : 'domain'), ['host' => $system->indexUrl()])
'theme' => $local ? 'warning' : 'info',
'text' => I18n::template('license.activate.' . ($local ? 'local' : 'domain'), ['host' => $system->indexUrl()])
],
'license' => [
'label' => I18n::translate('license.register.label'),
'label' => I18n::translate('license.code.label'),
'type' => 'text',
'required' => true,
'counter' => false,
'placeholder' => 'K3-',
'help' => I18n::translate('license.register.help')
'placeholder' => 'K-',
'help' => I18n::translate('license.code.help') . ' ' . '<a href="https://getkirby.com/buy" target="_blank">' . I18n::translate('license.buy') . ' &rarr;</a>'
],
'email' => Field::email(['required' => true])
],
'submitButton' => I18n::translate('license.register'),
'submitButton' => [
'icon' => 'key',
'text' => I18n::translate('activate'),
'theme' => 'love',
],
'value' => [
'license' => null,
'email' => null
@@ -83,7 +105,7 @@ return [
return [
'event' => 'system.register',
'message' => I18n::translate('license.register.success')
'message' => I18n::translate('license.success')
];
// @codeCoverageIgnoreEnd
}

View File

@@ -14,28 +14,29 @@ return [
$environment = [
[
'label' => $license ? I18n::translate('license') : I18n::translate('license.register.label'),
'value' => $license ? 'Kirby 3' : I18n::translate('license.unregistered.label'),
'theme' => $license ? null : 'negative',
'dialog' => $license ? 'license' : 'registration'
'label' => $license->status()->label(),
'value' => $license->label(),
'theme' => $license->status()->theme(),
'icon' => $license->status()->icon(),
'dialog' => $license->status()->dialog()
],
[
'label' => $updateStatus?->label() ?? I18n::translate('version'),
'value' => $kirby->version(),
'link' => (
$updateStatus ?
$updateStatus->url() :
'https://github.com/getkirby/kirby/releases/tag/' . $kirby->version()
),
'theme' => $updateStatus?->theme()
'link' => $updateStatus?->url() ??
'https://github.com/getkirby/kirby/releases/tag/' . $kirby->version(),
'theme' => $updateStatus?->theme(),
'icon' => $updateStatus?->icon() ?? 'info'
],
[
'label' => 'PHP',
'value' => phpversion()
'value' => phpversion(),
'icon' => 'code'
],
[
'label' => I18n::translate('server'),
'value' => $system->serverSoftware() ?? '?'
'value' => $system->serverSoftware() ?? '?',
'icon' => 'server'
]
];

View File

@@ -9,6 +9,7 @@ return function ($kirby) {
'search' => 'users',
'menu' => true,
'dialogs' => require __DIR__ . '/users/dialogs.php',
'drawers' => require __DIR__ . '/users/drawers.php',
'dropdowns' => require __DIR__ . '/users/dropdowns.php',
'searches' => require __DIR__ . '/users/searches.php',
'views' => require __DIR__ . '/users/views.php'

View File

@@ -6,9 +6,11 @@ use Kirby\Cms\UserRules;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Panel\Field;
use Kirby\Panel\Panel;
use Kirby\Panel\UserTotpDisableDialog;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
$fields = require __DIR__ . '/../fields/dialogs.php';
$files = require __DIR__ . '/../files/dialogs.php';
return [
@@ -18,6 +20,12 @@ return [
'pattern' => 'users/create',
'load' => function () {
$kirby = App::instance();
// get default value for role
if ($role = $kirby->request()->get('role')) {
$role = $kirby->roles()->find($role)?->id();
}
return [
'component' => 'k-form-dialog',
'props' => [
@@ -41,7 +49,7 @@ return [
'email' => '',
'password' => '',
'translation' => $kirby->panelLanguage(),
'role' => $kirby->user()->role()->name()
'role' => $role ?? $kirby->user()->role()->name()
]
]
];
@@ -287,6 +295,13 @@ return [
}
],
// user field dialogs
'user.fields' => [
'pattern' => '(users/.*?)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit']
],
// change file name
'user.file.changeName' => [
'pattern' => '(users/.*?)/files/(:any)/changeName',
@@ -301,11 +316,31 @@ return [
'submit' => $files['changeSort']['submit'],
],
// change file template
'user.file.changeTemplate' => [
'pattern' => '(users/.*?)/files/(:any)/changeTemplate',
'load' => $files['changeTemplate']['load'],
'submit' => $files['changeTemplate']['submit'],
],
// delete file
'user.file.delete' => [
'pattern' => '(users/.*?)/files/(:any)/delete',
'load' => $files['delete']['load'],
'submit' => $files['delete']['submit'],
]
],
// user file fields dialogs
'user.file.fields' => [
'pattern' => '(users/.*?)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit']
],
// user disable TOTP
'user.totp.disable' => [
'pattern' => 'users/(:any)/totp/disable',
'load' => fn (string $id) => (new UserTotpDisableDialog($id))->load(),
'submit' => fn (string $id) => (new UserTotpDisableDialog($id))->submit()
],
];

View File

@@ -0,0 +1,18 @@
<?php
$fields = require __DIR__ . '/../fields/drawers.php';
return [
// user field drawers
'user.fields' => [
'pattern' => '(users/.*?)/fields/(:any)/(:all?)',
'load' => $fields['model']['load'],
'submit' => $fields['model']['submit']
],
// user file fields drawers
'user.file.fields' => [
'pattern' => '(users/.*?)/files/(:any)/fields/(:any)/(:all?)',
'load' => $fields['file']['load'],
'submit' => $fields['file']['submit']
],
];

View File

@@ -8,20 +8,22 @@ return [
'users' => [
'label' => I18n::translate('users'),
'icon' => 'users',
'query' => function (string $query = null) {
$users = App::instance()->users()->search($query)->limit(10);
$results = [];
'query' => function (string $query = null, int $limit, int $page) {
$kirby = App::instance();
$users = $kirby->users()
->search($query)
->paginate($limit, $page);
foreach ($users as $user) {
$results[] = [
return [
'results' => $users->values(fn ($user) => [
'image' => $user->panel()->image(),
'text' => Escape::html($user->username()),
'link' => $user->panel()->url(true),
'info' => Escape::html($user->role()->title())
];
}
return $results;
'info' => Escape::html($user->role()->title()),
'uuid' => $user->uuid()->toString(),
]),
'pagination' => $users->pagination()->toArray()
];
}
]
];

View File

@@ -31,6 +31,10 @@ return [
$users = $users->role($role);
}
// sort users alphabetically
$users = $users->sortBy('username', 'asc');
// paginate
$users = $users->paginate([
'limit' => 20,
'page' => $kirby->request()->get('page')

View File

@@ -8,7 +8,7 @@ fields:
query: model.images
multiple: true
layout: cards
size: tiny
size: small
empty: field.blocks.gallery.images.empty
uploads:
template: blocks/image

View File

@@ -5,20 +5,31 @@ preview: heading
fields:
level:
label: field.blocks.heading.level
type: select
type: toggles
empty: false
default: "h2"
width: 1/6
labels: false
options:
- h1
- h2
- h3
- h4
- h5
- h6
- value: h1
icon: h1
text: H1
- value: h2
icon: h2
text: H2
- value: h3
icon: h3
text: H3
- value: h4
icon: h4
text: H4
- value: h5
icon: h5
text: H5
- value: h6
icon: h6
text: H6
text:
label: field.blocks.heading.text
type: writer
inline: true
width: 5/6
placeholder: field.blocks.heading.placeholder

View File

@@ -8,8 +8,8 @@ fields:
columns: 2
default: "kirby"
options:
kirby: Kirby
web: Web
kirby: "{{ t('field.blocks.image.location.internal') }}"
web: "{{ t('field.blocks.image.location.external') }}"
image:
label: field.blocks.image.name
type: files

View File

@@ -1,56 +0,0 @@
name: Code
icon: code
fields:
code:
label: Code
type: textarea
buttons: false
font: monospace
language:
label: Language
type: select
default: text
options:
bash: Bash
basic: BASIC
c: C
clojure: Clojure
cpp: C++
csharp: C#
css: CSS
diff: Diff
elixir: Elixir
elm: Elm
erlang: Erlang
go: Go
graphql: GraphQL
haskell: Haskell
html: HTML
java: Java
js: JavaScript
json: JSON
latext: LaTeX
less: Less
lisp: Lisp
lua: Lua
makefile: Makefile
markdown: Markdown
markup: Markup
objectivec: Objective-C
pascal: Pascal
perl: Perl
php: PHP
text: Plain Text
python: Python
r: R
ruby: Ruby
rust: Rust
sass: Sass
scss: SCSS
shell: Shell
sql: SQL
swift: Swift
typescript: TypeScript
vbnet: VB.net
xml: XML
yaml: YAML

View File

@@ -1,20 +0,0 @@
icon: title
fields:
text:
type: text
level:
type: select
width: 1/2
empty: false
default: "2"
options:
- value: "1"
text: Heading 1
- value: "2"
text: Heading 2
- value: "3"
text: Heading 3
id:
type: text
label: ID
width: 1/2

View File

@@ -1,16 +0,0 @@
name: Image
icon: image
fields:
image:
type: files
multiple: false
alt:
type: text
icon: title
caption:
type: writer
inline: true
icon: text
link:
type: text
icon: url

View File

@@ -1,12 +0,0 @@
name: Quote
icon: quote
fields:
text:
label: Quote Text
type: writer
inline: true
citation:
label: Citation
type: writer
inline: true
placeholder: by …

View File

@@ -1,25 +0,0 @@
name: Table
icon: menu
fields:
rows:
label: Menu
type: structure
columns:
dish: true
description: true
price:
before:
width: 1/4
align: right
fields:
dish:
label: Dish
type: text
description:
label: Description
type: text
price:
label: Price
type: number
before:
step: 0.01

View File

@@ -1,5 +0,0 @@
name: Text
icon: text
fields:
text:
type: writer

View File

@@ -1,8 +0,0 @@
name: Video
icon: video
label: "{{ url }}"
fields:
url:
type: url
caption:
type: writer

View File

@@ -1,2 +0,0 @@
name: File
title: file

View File

@@ -1,3 +0,0 @@
name: Page
title: Page

View File

@@ -1,7 +0,0 @@
name: Site
title: Site
sections:
pages:
headline: Pages
type: pages

View File

@@ -8,6 +8,7 @@ use Kirby\Cms\Page;
use Kirby\Cms\User;
use Kirby\Data\Data;
use Kirby\Email\PHPMailer as Emailer;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\F;
use Kirby\Filesystem\Filename;
use Kirby\Http\Uri;
@@ -19,13 +20,13 @@ use Kirby\Text\Markdown;
use Kirby\Text\SmartyPants;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
return [
/**
* Used by the `css()` helper
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $url Relative or absolute URL
* @param string|array $options An array of attributes for the link tag or a media attribute string
*/
@@ -33,35 +34,39 @@ return [
/**
* Add your own email provider
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param array $props
* @param bool $debug
*/
'email' => function (App $kirby, array $props = [], bool $debug = false) {
'email' => function (
App $kirby,
array $props = [],
bool $debug = false
) {
return new Emailer($props, $debug);
},
/**
* Modify URLs for file objects
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param \Kirby\Cms\File $file The original file object
* @return string
*/
'file::url' => function (App $kirby, File $file): string {
'file::url' => function (
App $kirby,
File $file
): string {
return $file->mediaUrl();
},
/**
* Adapt file characteristics
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param \Kirby\Cms\File|\Kirby\Filesystem\Asset $file The file object
* @param array $options All thumb options (width, height, crop, blur, grayscale)
* @return \Kirby\Cms\File|\Kirby\Cms\FileVersion|\Kirby\Filesystem\Asset
*/
'file::version' => function (App $kirby, $file, array $options = []) {
'file::version' => function (
App $kirby,
$file,
array $options = []
) {
// if file is not resizable, return
if ($file->isResizable() === false) {
return $file;
@@ -100,7 +105,6 @@ return [
/**
* Used by the `js()` helper
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $url Relative or absolute URL
* @param string|array $options An array of attributes for the link tag or a media attribute string
*/
@@ -109,10 +113,8 @@ return [
/**
* Add your own Markdown parser
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $text Text to parse
* @param array $options Markdown options
* @return string
*/
'markdown' => function (
App $kirby,
@@ -140,9 +142,9 @@ return [
'search' => function (
App $kirby,
Collection $collection,
string|null $query = '',
array|string $params = []
): Collection|bool {
string|null $query = null,
string|array $params = []
): Collection {
if (is_string($params) === true) {
$params = ['fields' => Str::split($params, '|')];
}
@@ -154,8 +156,9 @@ return [
'words' => false,
];
$options = array_merge($defaults, $params);
$query = trim($query ?? '');
$collection = clone $collection;
$options = array_merge($defaults, $params);
$query = trim($query ?? '');
// empty or too short search query
if (Str::length($query) < $options['minlength']) {
@@ -262,12 +265,14 @@ return [
/**
* Add your own SmartyPants parser
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $text Text to parse
* @param array $options SmartyPants options
* @return string
*/
'smartypants' => function (App $kirby, string $text = null, array $options = []): string {
'smartypants' => function (
App $kirby,
string $text = null,
array $options = []
): string {
static $smartypants;
static $config;
@@ -284,43 +289,55 @@ return [
/**
* Add your own snippet loader
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string|array $name Snippet name
* @param array $data Data array for the snippet
*/
'snippet' => function (App $kirby, string|array|null $name, array $data = [], bool $slots = false): Snippet|string {
'snippet' => function (
App $kirby,
string|array|null $name,
array $data = [],
bool $slots = false
): Snippet|string {
return Snippet::factory($name, $data, $slots);
},
/**
* Add your own template engine
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $name Template name
* @param string $type Extension type
* @param string $defaultType Default extension type
* @return \Kirby\Template\Template
*/
'template' => function (App $kirby, string $name, string $type = 'html', string $defaultType = 'html') {
'template' => function (
App $kirby,
string $name,
string $type = 'html',
string $defaultType = 'html'
) {
return new Template($name, $type, $defaultType);
},
/**
* Add your own thumb generator
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string $src Root of the original file
* @param string $dst Template string for the root to the desired destination
* @param array $options All thumb options that should be applied: `width`, `height`, `crop`, `blur`, `grayscale`
* @return string
*/
'thumb' => function (App $kirby, string $src, string $dst, array $options): string {
'thumb' => function (
App $kirby,
string $src,
string $dst,
array $options
): string {
$darkroom = Darkroom::factory(
$kirby->option('thumbs.driver', 'gd'),
$kirby->option('thumbs', [])
);
$options = $darkroom->preprocess($src, $options);
$root = (new Filename($src, $dst, $options))->toString();
$options = $darkroom->preprocess($src, $options);
$root = (new Filename($src, $dst, $options))->toString();
F::copy($src, $root, true);
$darkroom->process($root, $options);
@@ -331,12 +348,15 @@ return [
/**
* Modify all URLs
*
* @param \Kirby\Cms\App $kirby Kirby instance
* @param string|null $path URL path
* @param array|string|null $options Array of options for the Uri class
* @return string
* @throws \Kirby\Exception\NotFoundException If an invalid UUID was passed
*/
'url' => function (App $kirby, string $path = null, $options = null): string {
'url' => function (
App $kirby,
string $path = null,
$options = null
): string {
$language = null;
// get language from simple string option
@@ -378,6 +398,23 @@ return [
return $path;
}
// support UUIDs
if (
$path !== null &&
(
Uuid::is($path, 'page') === true ||
Uuid::is($path, 'file') === true
)
) {
$model = Uuid::for($path)->model();
if ($model === null) {
throw new NotFoundException('The model could not be found for "' . $path . '" uuid');
}
$path = $model->url();
}
$url = Url::makeAbsolute($path, $kirby->url());
if ($options === null) {

View File

@@ -0,0 +1,98 @@
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
return [
'props' => [
/**
* Unset inherited props
*/
'after' => null,
'before' => null,
/**
* Whether to allow alpha transparency in the color
*/
'alpha' => function (bool $alpha = false) {
return $alpha;
},
/**
* The CSS format (hex, rgb, hsl) to display and store the value
*/
'format' => function (string $format = 'hex'): string {
if (in_array($format, ['hex', 'hsl', 'rgb']) === false) {
throw new InvalidArgumentException('Unsupported format for color field (supported: hex, rgb, hsl)');
}
return $format;
},
/**
* Change mode to disable the color picker (`input`) or to only
* show the `options` as toggles
*/
'mode' => function (string $mode = 'picker'): string {
if (in_array($mode, ['picker', 'input', 'options']) === false) {
throw new InvalidArgumentException('Unsupported mode for color field (supported: picker, input, options)');
}
return $mode;
},
/**
* List of colors that will be shown as buttons
* to directly select them
*/
'options' => function (array $options = []): array {
return $options;
}
],
'computed' => [
'default' => function (): string {
return Str::lower($this->default);
},
'options' => function (): array {
return A::map(array_keys($this->options), fn ($key) => [
'value' => $this->options[$key],
'text' => is_string($key) ? $key : null
]);
}
],
'validations' => [
'color' => function ($value) {
if (empty($value) === true) {
return true;
}
if (
$this->format === 'hex' &&
preg_match('/^#([\da-f]{3,4}){1,2}$/i', $value) !== 1
) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'hex']
]);
}
if (
$this->format === 'rgb' &&
preg_match('/^rgba?\(\s*(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) !== 1
) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'rgb']
]);
}
if (
$this->format === 'hsl' &&
preg_match('/^hsla?\(\s*(\d{1,3}\.?\d*)(deg|rad|grad|turn)?(?:,|\s)+(\d{1,3})%(?:,|\s)+(\d{1,3})%(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) !== 1
) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'hsl']
]);
}
}
]
];

View File

@@ -1,5 +1,6 @@
<?php
use Kirby\Cms\ModelWithContent;
use Kirby\Data\Data;
use Kirby\Toolkit\A;
@@ -36,7 +37,10 @@ return [
'parentModel' => function () {
if (
is_string($this->parent) === true &&
$model = $this->model()->query($this->parent, 'Kirby\Cms\Model')
$model = $this->model()->query(
$this->parent,
ModelWithContent::class
)
) {
return $model;
}

View File

@@ -14,13 +14,6 @@ return [
'icon' => null,
'placeholder' => null,
'required' => null,
'translate' => null,
/**
* If `false`, the prepended number will be hidden
*/
'numbered' => function (bool $numbered = true) {
return $numbered;
}
'translate' => null
]
];

View File

@@ -1,3 +1,5 @@
<?php
return [];
return [
'hidden' => true
];

View File

@@ -12,7 +12,6 @@ return [
'before' => null,
'default' => null,
'disabled' => null,
'icon' => null,
'placeholder' => null,
'required' => null,
'translate' => null,

View File

@@ -0,0 +1,156 @@
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Http\Url;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
return [
'props' => [
'after' => null,
'before' => null,
'icon' => null,
'placeholder' => null,
/**
* @values 'anchor', 'url, 'page, 'file', 'email', 'tel', 'custom'
*/
'options' => function (array|null $options = null): array {
return $options ?? [
'url',
'page',
'file',
'email',
'tel',
'anchor',
'custom'
];
},
'value' => function (string|null $value = null) {
return $value ?? '';
}
],
'methods' => [
'activeTypes' => function () {
return array_filter($this->availableTypes(), function (string $type) {
return in_array($type, $this->props['options']) === true;
}, ARRAY_FILTER_USE_KEY);
},
'availableTypes' => function () {
return [
'anchor' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, '#') === true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (string $value): bool {
return Str::startsWith($value, '#') === true;
},
],
'email' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'mailto:') === true;
},
'link' => function (string $value): string {
return str_replace('mailto:', '', $value);
},
'validate' => function (string $value): bool {
return V::email($value);
},
],
'file' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'file://') === true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (string $value): bool {
return V::uuid($value, 'file');
},
],
'page' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'page://') === true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (string $value): bool {
return V::uuid($value, 'page');
},
],
'tel' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'tel:') === true;
},
'link' => function (string $value): string {
return str_replace('tel:', '', $value);
},
'validate' => function (string $value): bool {
return V::tel($value);
},
],
'url' => [
'detect' => function (string $value): bool {
return Str::startsWith($value, 'http://') === true || Str::startsWith($value, 'https://') === true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (string $value): bool {
return V::url($value);
},
],
// needs to come last
'custom' => [
'detect' => function (string $value): bool {
return true;
},
'link' => function (string $value): string {
return $value;
},
'validate' => function (): bool {
return true;
},
]
];
},
],
'validations' => [
'value' => function (string|null $value) {
if (empty($value) === true) {
return true;
}
$detected = false;
foreach ($this->activeTypes() as $type => $options) {
if ($options['detect']($value) !== true) {
continue;
}
$link = $options['link']($value);
$detected = true;
if ($options['validate']($link) === false) {
throw new InvalidArgumentException([
'key' => 'validation.' . $type
]);
}
}
// none of the configured types has been detected
if ($detected === false) {
throw new InvalidArgumentException([
'key' => 'validation.linkType'
]);
}
return true;
},
]
];

View File

@@ -7,6 +7,12 @@ return [
*/
'marks' => function ($marks = true) {
return $marks;
},
/**
* Sets the allowed nodes. Available nodes: `bulletList`, `orderedList`
*/
'nodes' => function ($nodes = null) {
return $nodes;
}
],
'computed' => [

View File

@@ -12,7 +12,7 @@ return [
},
/**
* Layout size for cards: `tiny`, `small`, `medium`, `large` or `huge`
* Layout size for cards: `tiny`, `small`, `medium`, `large`, `huge`, `full`
*/
'size' => function (string $size = 'auto') {
return $size;

View File

@@ -36,7 +36,7 @@ return [
},
'sanitizeOption' => function ($value) {
$options = array_column($this->options(), 'value');
return in_array($value, $options, true) === true ? $value : null;
return in_array($value, $options) === true ? $value : null;
},
'sanitizeOptions' => function ($values) {
$options = array_column($this->options(), 'value');

View File

@@ -3,6 +3,7 @@
use Kirby\Cms\Api;
use Kirby\Cms\File;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
return [
'props' => [
@@ -22,18 +23,23 @@ return [
$uploads = [];
}
$template = $uploads['template'] ?? null;
$uploads['accept'] = '*';
if ($template = $uploads['template'] ?? null) {
// get parent object for upload target
$parent = $this->uploadParent($uploads['parent'] ?? null);
if ($parent === null) {
throw new InvalidArgumentException('"' . $uploads['parent'] . '" could not be resolved as a valid parent for the upload');
}
if ($template) {
$file = new File([
'filename' => 'tmp',
'parent' => $this->model(),
'parent' => $parent,
'template' => $template
]);
$uploads['accept'] = $file->blueprint()->acceptMime();
} else {
$uploads['accept'] = '*';
}
return $uploads;
@@ -45,15 +51,7 @@ return [
throw new Exception('Uploads are disabled for this field');
}
if ($parentQuery = ($params['parent'] ?? null)) {
$parent = $this->model()->query($parentQuery);
} else {
$parent = $this->model();
}
if ($parent instanceof File) {
$parent = $parent->parent();
}
$parent = $this->uploadParent($params['parent'] ?? null);
return $api->upload(function ($source, $filename) use ($parent, $params, $map) {
$props = [
@@ -71,6 +69,19 @@ return [
return $map($file, $parent);
});
},
'uploadParent' => function (string $parentQuery = null) {
$parent = $this->model();
if ($parentQuery) {
$parent = $parent->query($parentQuery);
}
if ($parent instanceof File) {
$parent = $parent->parent();
}
return $parent;
}
]
];

View File

@@ -1,35 +1,23 @@
<?php
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
return [
'extends' => 'tags',
'props' => [
/**
* Unset inherited props
* If set to `all`, any type of input is accepted. If set to `options` only the predefined options are accepted as input.
*/
'accept' => null,
'accept' => function ($value = 'options') {
return V::in($value, ['all', 'options']) ? $value : 'all';
},
/**
* Custom icon to replace the arrow down.
*/
'icon' => function (string $icon = null) {
'icon' => function (string $icon = 'checklist') {
return $icon;
},
/**
* Enable/disable the search in the dropdown
* Also limit displayed items (display: 20)
* and set minimum number of characters to search (min: 3)
*/
'search' => function ($search = true) {
return $search;
},
/**
* If `true`, selected entries will be sorted
* according to their position in the dropdown
*/
'sort' => function (bool $sort = false) {
return $sort;
},
],
'methods' => [
'toValues' => function ($value) {

View File

@@ -47,7 +47,7 @@ return [
},
'fields' => function () {
if (empty($this->fields) === true) {
throw new Exception('Please provide some fields for the object');
return [];
}
return $this->form()->fields()->toArray();

View File

@@ -1,8 +1,11 @@
<?php
use Kirby\Data\Data;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Form\Form;
use Kirby\Toolkit\A;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
return [
'mixins' => ['min'],
@@ -49,7 +52,7 @@ return [
/**
* Fields setup for the structure form. Works just like fields in regular forms.
*/
'fields' => function (array $fields) {
'fields' => function (array $fields = []) {
return $fields;
},
/**
@@ -99,57 +102,54 @@ return [
},
'fields' => function () {
if (empty($this->fields) === true) {
throw new Exception('Please provide some fields for the structure');
return [];
}
return $this->form()->fields()->toArray();
},
'columns' => function () {
$columns = [];
$mobile = 0;
$columns = [];
$blueprint = $this->columns;
if (empty($this->columns) === true) {
foreach ($this->fields as $field) {
// Skip hidden and unsaveable fields
// They should never be included as column
if ($field['type'] === 'hidden' || $field['saveable'] === false) {
continue;
}
// if no custom columns have been defined,
// gather all fields as columns
if (empty($blueprint) === true) {
// skip hidden fields
$fields = array_filter(
$this->fields,
fn ($field) =>
$field['type'] !== 'hidden' && $field['hidden'] !== true
);
$fields = array_column($fields, 'name');
$blueprint = array_fill_keys($fields, true);
}
$columns[$field['name']] = [
'type' => $field['type'],
'label' => $field['label'] ?? $field['name']
];
foreach ($blueprint as $name => $column) {
$field = $this->fields[$name] ?? null;
// Skip empty and unsaveable fields
// They should never be included as column
if (
empty($field) === true ||
$field['saveable'] === false
) {
continue;
}
} else {
foreach ($this->columns as $columnName => $columnProps) {
if (is_array($columnProps) === false) {
$columnProps = [];
}
$field = $this->fields[$columnName] ?? null;
if (
empty($field) === true ||
$field['saveable'] === false
) {
continue;
}
if (($columnProps['mobile'] ?? false) === true) {
$mobile++;
}
$columns[$columnName] = array_merge([
'type' => $field['type'],
'label' => $field['label'] ?? $field['name']
], $columnProps);
if (is_array($column) === false) {
$column = [];
}
$column['type'] ??= $field['type'];
$column['label'] ??= $field['label'] ?? $name;
$column['label'] = I18n::translate($column['label'], $column['label']);
$columns[$name] = $column;
}
// make the first column visible on mobile
// if no other mobile columns are defined
if ($mobile === 0) {
if (in_array(true, array_column($columns, 'mobile')) === false) {
$columns[array_key_first($columns)]['mobile'] = true;
}
@@ -179,28 +179,47 @@ return [
]);
},
],
'api' => function () {
return [
[
'pattern' => 'validate',
'method' => 'ALL',
'action' => function () {
return array_values($this->field()->form($this->requestBody())->errors());
}
]
];
},
'save' => function ($value) {
$data = [];
foreach ($value as $row) {
$data[] = $this->form($row)->content();
$row = $this->form($row)->content();
// remove frontend helper id
unset($row['_id']);
$data[] = $row;
}
return $data;
},
'validations' => [
'min',
'max'
'max',
'structure' => function ($value) {
if (empty($value) === true) {
return true;
}
$values = A::wrap($value);
foreach ($values as $index => $value) {
$form = $this->form($value);
foreach ($form->fields() as $field) {
$errors = $field->errors();
if (empty($errors) === false) {
throw new InvalidArgumentException([
'key' => 'structure.validation',
'data' => [
'field' => $field->label() ?? Str::ucfirst($field->name()),
'index' => $index + 1
]
]);
}
}
}
}
]
];

View File

@@ -46,12 +46,27 @@ return [
'max' => function (int $max = null) {
return $max;
},
/**
* Enable/disable the search in the dropdown
* Also limit displayed items (display: 20)
* and set minimum number of characters to search (min: 3)
*/
'search' => function (bool|array $search = true) {
return $search;
},
/**
* Custom tags separator, which will be used to store tags in the content file
*/
'separator' => function (string $separator = ',') {
return $separator;
},
/**
* If `true`, selected entries will be sorted
* according to their position in the dropdown
*/
'sort' => function (bool $sort = false) {
return $sort;
},
],
'computed' => [
'default' => function (): array {

View File

@@ -27,6 +27,13 @@ return [
return $counter;
},
/**
* Sets the font family (sans or monospace)
*/
'font' => function (string $font = null) {
return $font === 'monospace' ? 'monospace' : 'sans-serif';
},
/**
* Maximum number of allowed characters
*/

View File

@@ -24,18 +24,8 @@ return [
/**
* Default selected user(s) when a new page/file/user is created
*/
'default' => function ($default = null) {
if ($default === false) {
return [];
}
if ($default === null && $user = $this->kirby()->user()) {
return [
$this->userResponse($user)
];
}
return $this->toUsers($default);
'default' => function (string|array|bool|null $default = null) {
return $default;
},
'value' => function ($value = null) {
@@ -43,10 +33,22 @@ return [
},
],
'computed' => [
/**
* Unset inherited computed
*/
'default' => null
'default' => function (): array {
if ($this->default === false) {
return [];
}
if (
$this->default === true &&
$user = $this->kirby()->user()
) {
return [
$this->userResponse($user)
];
}
return $this->toUsers($this->default);
}
],
'methods' => [
'userResponse' => function ($user) {
@@ -57,7 +59,7 @@ return [
'text' => $this->text,
]);
},
'toUsers' => function ($value = null) {
'toUsers' => function ($value = null): array {
$users = [];
$kirby = App::instance();

View File

@@ -1,9 +1,23 @@
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Sane\Sane;
use Kirby\Toolkit\V;
return [
'props' => [
/**
* Enables/disables the character counter in the top right corner
*/
'counter' => function (bool $counter = true) {
return $counter;
},
/**
* Available heading levels
*/
'headings' => function (array|null $headings = null) {
return array_intersect($headings ?? range(1, 6), range(1, 6));
},
/**
* Enables inline mode, which will not wrap new lines in paragraphs and creates hard breaks instead.
*
@@ -13,18 +27,37 @@ return [
return $inline;
},
/**
* Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`, `email`. Activate them all by passing `true`. Deactivate them all by passing `false`
* Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`, `email`. Activate/deactivate them all by passing `true`/`false`. Default marks are `bold`, `italic`, `underline`, `strike`, `link`, `email`
* @param array|bool $marks
*/
'marks' => function ($marks = true) {
'marks' => function ($marks = null) {
return $marks;
},
/**
* Sets the allowed nodes. Available nodes: `paragraph`, `heading`, `bulletList`, `orderedList`. Activate/deactivate them all by passing `true`/`false`. Default nodes are `paragraph`, `heading`, `bulletList`, `orderedList`.
* Maximum number of allowed characters
*/
'maxlength' => function (int $maxlength = null) {
return $maxlength;
},
/**
* Minimum number of required characters
*/
'minlength' => function (int $minlength = null) {
return $minlength;
},
/**
* Sets the allowed nodes. Available nodes: `paragraph`, `heading`, `bulletList`, `orderedList`, `quote`. Activate/deactivate them all by passing `true`/`false`. Default nodes are `paragraph`, `heading`, `bulletList`, `orderedList`.
* @param array|bool|null $nodes
*/
'nodes' => function ($nodes = null) {
return $nodes;
},
/**
* Toolbar options, incl. `marks` (to narrow down which marks should have toolbar buttons), `nodes` (to narrow down which nodes should have toolbar dropdown entries) and `inline` to set the position of the toolbar (false = sticking on top of the field)
*/
'toolbar' => function ($toolbar = null) {
return $toolbar;
}
],
'computed' => [
@@ -33,4 +66,28 @@ return [
return Sane::sanitize($value, 'html');
}
],
'validations' => [
'minlength' => function ($value) {
if (
$this->minlength &&
V::minLength(strip_tags($value), $this->minlength) === false
) {
throw new InvalidArgumentException([
'key' => 'validation.minlength',
'data' => ['min' => $this->minlength]
]);
}
},
'maxlength' => function ($value) {
if (
$this->maxlength &&
V::maxLength(strip_tags($value), $this->maxlength) === false
) {
throw new InvalidArgumentException([
'key' => 'validation.maxlength',
'data' => ['max' => $this->maxlength]
]);
}
},
]
];

View File

@@ -4,6 +4,7 @@ use Kirby\Cms\App;
use Kirby\Cms\File;
use Kirby\Cms\Helpers;
use Kirby\Cms\Html;
use Kirby\Cms\ModelWithContent;
use Kirby\Cms\Page;
use Kirby\Cms\Pages;
use Kirby\Cms\Response;
@@ -12,6 +13,7 @@ use Kirby\Cms\Url;
use Kirby\Filesystem\Asset;
use Kirby\Filesystem\F;
use Kirby\Http\Router;
use Kirby\Image\QrCode;
use Kirby\Template\Slot;
use Kirby\Template\Snippet;
use Kirby\Toolkit\Date;
@@ -195,10 +197,8 @@ if (Helpers::hasOverride('go') === false) { // @codeCoverageIgnore
/**
* Redirects to the given Urls
* Urls can be relative or absolute.
*
* @todo Change return type to `never` once support for PHP 8.0 is dropped
*/
function go(string $url = '/', int $code = 302): void
function go(string $url = '/', int $code = 302): never
{
Response::go($url, $code);
}
@@ -434,6 +434,20 @@ if (Helpers::hasOverride('params') === false) { // @codeCoverageIgnore
}
}
if (Helpers::hasOverride('qr') === false) { // @codeCoverageIgnore
/**
* Creates a QR code object
*/
function qr(string|ModelWithContent $data): QrCode
{
if ($data instanceof ModelWithContent) {
$data = $data->url();
}
return new QrCode($data);
}
}
if (Helpers::hasOverride('r') === false) { // @codeCoverageIgnore
/**
* Smart version of return with an if condition as first argument
@@ -588,25 +602,6 @@ if (Helpers::hasOverride('tt') === false) { // @codeCoverageIgnore
}
}
if (Helpers::hasOverride('twitter') === false) { // @codeCoverageIgnore
/**
* Builds a Twitter link
*/
function twitter(
string $username,
string|null $text = null,
string|null $title = null,
string|null $class = null
): string {
return App::instance()->kirbytag([
'twitter' => $username,
'text' => $text,
'title' => $title,
'class' => $class
]);
}
}
if (Helpers::hasOverride('u') === false) { // @codeCoverageIgnore
/**
* Shortcut for url()
@@ -647,8 +642,11 @@ if (Helpers::hasOverride('video') === false) { // @codeCoverageIgnore
* videos. The embed Urls are automatically detected from
* the given Url.
*/
function video(string $url, array $options = [], array $attr = []): string|null
{
function video(
string $url,
array $options = [],
array $attr = []
): string|null {
return Html::video($url, $options, $attr);
}
}
@@ -657,8 +655,11 @@ if (Helpers::hasOverride('vimeo') === false) { // @codeCoverageIgnore
/**
* Embeds a Vimeo video by URL in an iframe
*/
function vimeo(string $url, array $options = [], array $attr = []): string|null
{
function vimeo(
string $url,
array $options = [],
array $attr = []
): string|null {
return Html::vimeo($url, $options, $attr);
}
}
@@ -679,8 +680,11 @@ if (Helpers::hasOverride('youtube') === false) { // @codeCoverageIgnore
/**
* Embeds a Youtube video by URL in an iframe
*/
function youtube(string $url, array $options = [], array $attr = []): string|null
{
function youtube(
string $url,
array $options = [],
array $attr = []
): string|null {
return Html::youtube($url, $options, $attr);
}
}

View File

@@ -2,19 +2,29 @@
use Kirby\Cms\App;
use Kirby\Cms\Blocks;
use Kirby\Cms\Content;
use Kirby\Cms\Field;
use Kirby\Cms\File;
use Kirby\Cms\Files;
use Kirby\Cms\Html;
use Kirby\Cms\Layouts;
use Kirby\Cms\Page;
use Kirby\Cms\Pages;
use Kirby\Cms\Structure;
use Kirby\Cms\Url;
use Kirby\Cms\User;
use Kirby\Cms\Users;
use Kirby\Content\Content;
use Kirby\Content\Field;
use Kirby\Data\Data;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Image\QrCode;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Dom;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
use Kirby\Toolkit\Xml;
use Kirby\Uuid\Uuid;
/**
* Field method setup
@@ -26,9 +36,6 @@ return function (App $app) {
/**
* Converts the field value into a proper boolean and inverts it
*
* @param \Kirby\Cms\Field $field
* @return bool
*/
'isFalse' => function (Field $field): bool {
return $field->toBool() === false;
@@ -36,9 +43,6 @@ return function (App $app) {
/**
* Converts the field value into a proper boolean
*
* @param \Kirby\Cms\Field $field
* @return bool
*/
'isTrue' => function (Field $field): bool {
return $field->toBool() === true;
@@ -47,22 +51,21 @@ return function (App $app) {
/**
* Validates the field content with the given validator and parameters
*
* @param string $validator
* @param mixed ...$arguments A list of optional validator arguments
* @return bool
*/
'isValid' => function (Field $field, string $validator, ...$arguments): bool {
'isValid' => function (
Field $field,
string $validator,
...$arguments
): bool {
return V::$validator($field->value, ...$arguments);
},
// converters
/**
* Converts a yaml or json field to a Blocks object
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Blocks
*/
'toBlocks' => function (Field $field) {
'toBlocks' => function (Field $field): Blocks {
try {
$blocks = Blocks::parse($field->value());
$blocks = Blocks::factory($blocks, [
@@ -84,11 +87,9 @@ return function (App $app) {
/**
* Converts the field value into a proper boolean
*
* @param \Kirby\Cms\Field $field
* @param bool $default Default value if the field is empty
* @return bool
*/
'toBool' => function (Field $field, $default = false): bool {
'toBool' => function (Field $field, bool $default = false): bool {
$value = $field->isEmpty() ? $default : $field->value;
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
},
@@ -96,11 +97,9 @@ return function (App $app) {
/**
* Parses the field value with the given method
*
* @param \Kirby\Cms\Field $field
* @param string $method [',', 'yaml', 'json']
* @return array
*/
'toData' => function (Field $field, string $method = ',') {
'toData' => function (Field $field, string $method = ','): array {
return match ($method) {
'yaml', 'json' => Data::decode($field->value, $method),
default => $field->split($method)
@@ -110,12 +109,14 @@ return function (App $app) {
/**
* Converts the field value to a timestamp or a formatted date
*
* @param \Kirby\Cms\Field $field
* @param string|\IntlDateFormatter|null $format PHP date formatting string
* @param string|null $fallback Fallback string for `strtotime` (since 3.2)
* @return string|int
* @param string|null $fallback Fallback string for `strtotime`
*/
'toDate' => function (Field $field, $format = null, string $fallback = null) use ($app) {
'toDate' => function (
Field $field,
string|IntlDateFormatter|null $format = null,
string $fallback = null
) use ($app): string|int|null {
if (empty($field->value) === true && $fallback === null) {
return null;
}
@@ -126,28 +127,23 @@ return function (App $app) {
$time = strtotime($fallback);
}
$handler = $app->option('date.handler', 'date');
return Str::date($time, $format, $handler);
return Str::date($time, $format);
},
/**
* Returns a file object from a filename in the field
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\File|null
*/
'toFile' => function (Field $field) {
'toFile' => function (Field $field): File|null {
return $field->toFiles()->first();
},
/**
* Returns a file collection from a yaml list of filenames in the field
*
* @param \Kirby\Cms\Field $field
* @param string $separator
* @return \Kirby\Cms\Files
*/
'toFiles' => function (Field $field, string $separator = 'yaml') {
'toFiles' => function (
Field $field,
string $separator = 'yaml'
): Files {
$parent = $field->parent();
$files = new Files([]);
@@ -163,11 +159,9 @@ return function (App $app) {
/**
* Converts the field value into a proper float
*
* @param \Kirby\Cms\Field $field
* @param float $default Default value if the field is empty
* @return float
*/
'toFloat' => function (Field $field, float $default = 0) {
'toFloat' => function (Field $field, float $default = 0): float {
$value = $field->isEmpty() ? $default : $field->value;
return (float)$value;
},
@@ -175,23 +169,17 @@ return function (App $app) {
/**
* Converts the field value into a proper integer
*
* @param \Kirby\Cms\Field $field
* @param int $default Default value if the field is empty
* @return int
*/
'toInt' => function (Field $field, int $default = 0) {
'toInt' => function (Field $field, int $default = 0): int {
$value = $field->isEmpty() ? $default : $field->value;
return (int)$value;
},
/**
* Parse layouts and turn them into
* Layout objects
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Layouts
* Parse layouts and turn them into Layout objects
*/
'toLayouts' => function (Field $field) {
'toLayouts' => function (Field $field): Layouts {
return Layouts::factory(Layouts::parse($field->value()), [
'parent' => $field->parent(),
'field' => $field,
@@ -201,12 +189,14 @@ return function (App $app) {
/**
* Wraps a link tag around the field value. The field value is used as the link text
*
* @param \Kirby\Cms\Field $field
* @param mixed $attr1 Can be an optional Url. If no Url is set, the Url of the Page, File or Site will be used. Can also be an array of link attributes
* @param mixed $attr2 If `$attr1` is used to set the Url, you can use `$attr2` to pass an array of additional attributes.
* @return string
*/
'toLink' => function (Field $field, $attr1 = null, $attr2 = null) {
'toLink' => function (
Field $field,
string|array|null $attr1 = null,
array|null $attr2 = null
): string {
if (is_string($attr1) === true) {
$href = $attr1;
$attr = $attr2;
@@ -225,49 +215,55 @@ return function (App $app) {
/**
* Parse yaml data and convert it to a
* content object
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Content
*/
'toObject' => function (Field $field) {
'toObject' => function (Field $field): Content {
return new Content($field->yaml(), $field->parent(), true);
},
/**
* Returns a page object from a page id in the field
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Page|null
*/
'toPage' => function (Field $field) {
'toPage' => function (Field $field): Page|null {
return $field->toPages()->first();
},
/**
* Returns a pages collection from a yaml list of page ids in the field
*
* @param \Kirby\Cms\Field $field
* @param string $separator Can be any other separator to split the field value by
* @return \Kirby\Cms\Pages
*/
'toPages' => function (Field $field, string $separator = 'yaml') use ($app) {
return $app->site()->find(false, false, ...$field->toData($separator));
'toPages' => function (
Field $field,
string $separator = 'yaml'
) use ($app): Pages {
return $app->site()->find(
false,
false,
...$field->toData($separator)
);
},
/**
* Turns the field value into an QR code object
*/
'toQrCode' => function (Field $field): QrCode|null {
return $field->isNotEmpty() ? new QrCode($field->value) : null;
},
/**
* Converts a yaml field to a Structure object
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Structure
*/
'toStructure' => function (Field $field) {
'toStructure' => function (Field $field): Structure {
try {
return new Structure(Data::decode($field->value, 'yaml'), $field->parent());
return Structure::factory(
Data::decode($field->value, 'yaml'),
['parent' => $field->parent()]
);
} catch (Exception) {
$message = 'Invalid structure data for "' . $field->key() . '" field';
if ($parent = $field->parent()) {
$message .= ' on parent "' . $parent->title() . '"';
$message .= ' on parent "' . $parent->id() . '"';
}
throw new InvalidArgumentException($message);
@@ -276,9 +272,6 @@ return function (App $app) {
/**
* Converts the field value to a Unix timestamp
*
* @param \Kirby\Cms\Field $field
* @return int|false
*/
'toTimestamp' => function (Field $field): int|false {
return strtotime($field->value ?? '');
@@ -286,33 +279,35 @@ return function (App $app) {
/**
* Turns the field value into an absolute Url
*
* @param \Kirby\Cms\Field $field
* @return string
*/
'toUrl' => function (Field $field): string {
return Url::to($field->value);
'toUrl' => function (Field $field): string|null {
try {
return $field->isNotEmpty() ? Url::to($field->value) : null;
} catch (NotFoundException) {
return null;
}
},
/**
* Converts a user email address to a user object
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\User|null
*/
'toUser' => function (Field $field) {
'toUser' => function (Field $field): User|null {
return $field->toUsers()->first();
},
/**
* Returns a users collection from a yaml list of user email addresses in the field
*
* @param \Kirby\Cms\Field $field
* @param string $separator
* @return \Kirby\Cms\Users
* Returns a users collection from a yaml list
* of user email addresses in the field
*/
'toUsers' => function (Field $field, string $separator = 'yaml') use ($app) {
return $app->users()->find(false, false, ...$field->toData($separator));
'toUsers' => function (
Field $field,
string $separator = 'yaml'
) use ($app): Users {
return $app->users()->find(
false,
false,
...$field->toData($separator)
);
},
// inspectors
@@ -320,14 +315,14 @@ return function (App $app) {
/**
* Returns the length of the field content
*/
'length' => function (Field $field) {
'length' => function (Field $field): int {
return Str::length($field->value);
},
/**
* Returns the number of words in the text
*/
'words' => function (Field $field) {
'words' => function (Field $field): int {
return str_word_count(strip_tags($field->value ?? ''));
},
@@ -336,11 +331,8 @@ return function (App $app) {
/**
* Applies the callback function to the field
* @since 3.4.0
*
* @param \Kirby\Cms\Field $field
* @param Closure $callback
*/
'callback' => function (Field $field, Closure $callback) {
'callback' => function (Field $field, Closure $callback): mixed {
return $callback($field);
},
@@ -348,10 +340,9 @@ return function (App $app) {
* Escapes the field value to be safely used in HTML
* templates without the risk of XSS attacks
*
* @param \Kirby\Cms\Field $field
* @param string $context Location of output (`html`, `attr`, `js`, `css`, `url` or `xml`)
*/
'escape' => function (Field $field, string $context = 'html') {
'escape' => function (Field $field, string $context = 'html'): Field {
$field->value = Str::esc($field->value ?? '', $context);
return $field;
},
@@ -359,25 +350,26 @@ return function (App $app) {
/**
* Creates an excerpt of the field value without html
* or any other formatting.
*
* @param \Kirby\Cms\Field $field
* @param int $cahrs
* @param bool $strip
* @param string $rep
* @return \Kirby\Cms\Field
*/
'excerpt' => function (Field $field, int $chars = 0, bool $strip = true, string $rep = ' …') {
$field->value = Str::excerpt($field->kirbytext()->value(), $chars, $strip, $rep);
'excerpt' => function (
Field $field,
int $chars = 0,
bool $strip = true,
string $rep = ' …'
): Field {
$field->value = Str::excerpt(
$field->kirbytext()->value(),
$chars,
$strip,
$rep
);
return $field;
},
/**
* Converts the field content to valid HTML
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'html' => function (Field $field) {
'html' => function (Field $field): Field {
$field->value = Html::encode($field->value);
return $field;
},
@@ -387,11 +379,8 @@ return function (App $app) {
* it can be safely placed inside of other inline elements
* without the risk of breaking the HTML structure.
* @since 3.3.0
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'inline' => function (Field $field) {
'inline' => function (Field $field): Field {
// List of valid inline elements taken from: https://developer.mozilla.org/de/docs/Web/HTML/Inline_elemente
// Obsolete elements, script tags, image maps and form elements have
// been excluded for safety reasons and as they are most likely not
@@ -402,12 +391,11 @@ return function (App $app) {
/**
* Converts the field content from Markdown/Kirbytext to valid HTML
*
* @param \Kirby\Cms\Field $field
* @param array $options
* @return \Kirby\Cms\Field
*/
'kirbytext' => function (Field $field, array $options = []) use ($app) {
'kirbytext' => function (
Field $field,
array $options = []
) use ($app): Field {
$field->value = $app->kirbytext($field->value, A::merge($options, [
'parent' => $field->parent(),
'field' => $field
@@ -420,12 +408,11 @@ return function (App $app) {
* Converts the field content from inline Markdown/Kirbytext
* to valid HTML
* @since 3.1.0
*
* @param \Kirby\Cms\Field $field
* @param array $options
* @return \Kirby\Cms\Field
*/
'kirbytextinline' => function (Field $field, array $options = []) use ($app) {
'kirbytextinline' => function (
Field $field,
array $options = []
) use ($app): Field {
$field->value = $app->kirbytext($field->value, A::merge($options, [
'parent' => $field->parent(),
'field' => $field,
@@ -439,11 +426,8 @@ return function (App $app) {
/**
* Parses all KirbyTags without also parsing Markdown
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'kirbytags' => function (Field $field) use ($app) {
'kirbytags' => function (Field $field) use ($app): Field {
$field->value = $app->kirbytags($field->value, [
'parent' => $field->parent(),
'field' => $field
@@ -454,23 +438,19 @@ return function (App $app) {
/**
* Converts the field content to lowercase
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'lower' => function (Field $field) {
'lower' => function (Field $field): Field {
$field->value = Str::lower($field->value);
return $field;
},
/**
* Converts markdown to valid HTML
*
* @param \Kirby\Cms\Field $field
* @param array $options
* @return \Kirby\Cms\Field
*/
'markdown' => function (Field $field, array $options = []) use ($app) {
'markdown' => function (
Field $field,
array $options = []
) use ($app): Field {
$field->value = $app->markdown($field->value, $options);
return $field;
},
@@ -478,23 +458,55 @@ return function (App $app) {
/**
* Converts all line breaks in the field content to `<br>` tags.
* @since 3.3.0
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'nl2br' => function (Field $field) {
'nl2br' => function (Field $field): Field {
$field->value = nl2br($field->value ?? '', false);
return $field;
},
/**
* Uses the field value as Kirby query
* Parses the field value as DOM and replaces
* any permalinks in href/src attributes with
* the regular url
*
* @param \Kirby\Cms\Field $field
* @param string|null $expect
* @return mixed
* This method is still experimental! You can use
* it to solve potential problems with permalinks
* already, but it might change in the future.
*/
'query' => function (Field $field, string $expect = null) use ($app) {
'permalinksToUrls' => function (Field $field): Field {
if ($field->isNotEmpty() === true) {
$dom = new Dom($field->value);
$attributes = ['href', 'src'];
$elements = $dom->query('//*[' . implode(' | ', A::map($attributes, fn ($attribute) => '@' . $attribute)) . ']');
foreach ($elements as $element) {
foreach ($attributes as $attribute) {
if ($element->hasAttribute($attribute) && $url = $element->getAttribute($attribute)) {
try {
if ($uuid = Uuid::for($url)) {
$url = $uuid->model()?->url();
$element->setAttribute($attribute, $url);
}
} catch (InvalidArgumentException) {
// ignore anything else than permalinks
}
}
}
}
$field->value = $dom->toString();
}
return $field;
},
/**
* Uses the field value as Kirby query
*/
'query' => function (
Field $field,
string $expect = null
) use ($app): mixed {
if ($parent = $field->parent()) {
return $parent->query($field->value, $expect);
}
@@ -509,13 +521,13 @@ return function (App $app) {
/**
* It parses any queries found in the field value.
*
* @param \Kirby\Cms\Field $field
* @param array $data
* @param string|null $fallback Fallback for tokens in the template that cannot be replaced
* (`null` to keep the original token)
* @return \Kirby\Cms\Field
* @param string|null $fallback Fallback for tokens in the template that cannot be replaced (`null` to keep the original token)
*/
'replace' => function (Field $field, array $data = [], string|null $fallback = '') use ($app) {
'replace' => function (
Field $field,
array $data = [],
string|null $fallback = ''
) use ($app): Field {
if ($parent = $field->parent()) {
// never pass `null` as the $template to avoid the fallback to the model ID
$field->value = $parent->toString($field->value ?? '', $data, $fallback);
@@ -534,55 +546,45 @@ return function (App $app) {
* Cuts the string after the given length and
* adds "…" if it is longer
*
* @param \Kirby\Cms\Field $field
* @param int $length The number of characters in the string
* @param string $appendix An optional replacement for the missing rest
* @return \Kirby\Cms\Field
*/
'short' => function (Field $field, int $length, string $appendix = '…') {
'short' => function (
Field $field,
int $length,
string $appendix = '…'
): Field {
$field->value = Str::short($field->value, $length, $appendix);
return $field;
},
/**
* Converts the field content to a slug
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'slug' => function (Field $field) {
'slug' => function (Field $field): Field {
$field->value = Str::slug($field->value);
return $field;
},
/**
* Applies SmartyPants to the field
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'smartypants' => function (Field $field) use ($app) {
'smartypants' => function (Field $field) use ($app): Field {
$field->value = $app->smartypants($field->value);
return $field;
},
/**
* Splits the field content into an array
*
* @param \Kirby\Cms\Field $field
* @return array
*/
'split' => function (Field $field, $separator = ',') {
'split' => function (Field $field, $separator = ','): array {
return Str::split((string)$field->value, $separator);
},
/**
* Converts the field content to uppercase
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'upper' => function (Field $field) {
'upper' => function (Field $field): Field {
$field->value = Str::upper($field->value);
return $field;
},
@@ -590,22 +592,16 @@ return function (App $app) {
/**
* Avoids typographical widows in strings by replacing
* the last space with `&nbsp;`
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'widont' => function (Field $field) {
'widont' => function (Field $field): Field {
$field->value = Str::widont($field->value);
return $field;
},
/**
* Converts the field content to valid XML
*
* @param \Kirby\Cms\Field $field
* @return \Kirby\Cms\Field
*/
'xml' => function (Field $field) {
'xml' => function (Field $field): Field {
$field->value = Xml::encode($field->value);
return $field;
},
@@ -614,9 +610,6 @@ return function (App $app) {
/**
* Parses yaml in the field content and returns an array
*
* @param \Kirby\Cms\Field $field
* @return array
*/
'yaml' => function (Field $field): array {
return $field->toData('yaml');

View File

@@ -1,5 +1,6 @@
<?php
use Kirby\Cms\App;
use Kirby\Cms\LanguageRoutes;
use Kirby\Cms\Media;
use Kirby\Cms\PluginAssets;
@@ -8,7 +9,7 @@ use Kirby\Panel\Plugins;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
return function ($kirby) {
return function (App $kirby) {
$api = $kirby->option('api.slug', 'api');
$panel = $kirby->option('panel.slug', 'panel');
$index = $kirby->url('index');
@@ -32,7 +33,7 @@ return function ($kirby) {
'pattern' => $api . '/(:all)',
'method' => 'ALL',
'env' => 'api',
'action' => function ($path = null) use ($kirby) {
'action' => function (string $path = null) use ($kirby) {
if ($kirby->option('api') === false) {
return null;
}
@@ -60,37 +61,63 @@ return function ($kirby) {
}
],
[
'pattern' => $media . '/plugins/(:any)/(:any)/(:all)\.(css|map|gif|js|mjs|jpg|png|svg|webp|avif|woff2|woff|json)',
// TODO: change to '/plugins/(:any)/(:any)/(:any)/(:all)' once
// the hash is made mandatory
'pattern' => $media . '/plugins/(:any)/(:any)/(?:(:any)/)?(:all)',
'env' => 'media',
'action' => function (string $provider, string $pluginName, string $filename, string $extension) {
return PluginAssets::resolve($provider . '/' . $pluginName, $filename . '.' . $extension);
'action' => function (
string $provider,
string $pluginName,
string $hash,
string $path
) {
return PluginAssets::resolve(
$provider . '/' . $pluginName,
$hash,
$path
);
}
],
[
'pattern' => $media . '/pages/(:all)/(:any)/(:any)',
'env' => 'media',
'action' => function ($path, $hash, $filename) use ($kirby) {
'action' => function (
string $path,
string $hash,
string $filename
) use ($kirby) {
return Media::link($kirby->page($path), $hash, $filename);
}
],
[
'pattern' => $media . '/site/(:any)/(:any)',
'env' => 'media',
'action' => function ($hash, $filename) use ($kirby) {
'action' => function (
string $hash,
string $filename
) use ($kirby) {
return Media::link($kirby->site(), $hash, $filename);
}
],
[
'pattern' => $media . '/users/(:any)/(:any)/(:any)',
'env' => 'media',
'action' => function ($id, $hash, $filename) use ($kirby) {
'action' => function (
string $id,
string $hash,
string $filename
) use ($kirby) {
return Media::link($kirby->user($id), $hash, $filename);
}
],
[
'pattern' => $media . '/assets/(:all)/(:any)/(:any)',
'env' => 'media',
'action' => function ($path, $hash, $filename) {
'action' => function (
string $path,
string $hash,
string $filename
) {
return Media::thumb($path, $hash, $filename);
}
],
@@ -98,7 +125,7 @@ return function ($kirby) {
'pattern' => $panel . '/(:all?)',
'method' => 'ALL',
'env' => 'panel',
'action' => function ($path = null) {
'action' => function (string $path = null) {
return Panel::router($path);
}
],

View File

@@ -1,6 +1,7 @@
<?php
use Kirby\Cms\File;
use Kirby\Cms\Files;
use Kirby\Toolkit\I18n;
return [
@@ -18,6 +19,12 @@ return [
'sort'
],
'props' => [
/**
* Filters pages by a query. Sorting will be disabled
*/
'query' => function (string|null $query = null) {
return $query;
},
/**
* Filters all files by template and also sets the template, which will be used for all uploads
*/
@@ -49,10 +56,17 @@ return [
return $this->parentModel();
},
'files' => function () {
$files = $this->parent->files()->template($this->template);
if ($this->query !== null) {
$files = $this->parent->query($this->query, Files::class) ?? new Files([]);
} else {
$files = $this->parent->files();
}
// filter out all protected files
$files = $files->filter('isReadable', true);
// filter files by template
$files = $files->template($this->template);
// filter out all protected and hidden files
$files = $files->filter('isListable', true);
// search
if ($this->search === true && empty($this->searchterm()) === false) {

View File

@@ -7,6 +7,9 @@ return [
'headline',
],
'props' => [
'icon' => function (string $icon = null) {
return $icon;
},
'text' => function ($text = null) {
return I18n::translate($text, $text);
},
@@ -25,6 +28,7 @@ return [
],
'toArray' => function () {
return [
'icon' => $this->icon,
'label' => $this->headline,
'text' => $this->text,
'theme' => $this->theme

View File

@@ -20,7 +20,7 @@ return [
return in_array($layout, $layouts) ? $layout : 'list';
},
/**
* The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge`
* The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge`, `full`
*/
'size' => function (string $size = 'auto') {
return $size;

View File

@@ -39,6 +39,10 @@ return [
return false;
}
if ($this->query !== null) {
return false;
}
if ($this->sortBy !== null) {
return false;
}

View File

@@ -2,6 +2,7 @@
use Kirby\Cms\Blueprint;
use Kirby\Cms\Page;
use Kirby\Cms\Pages;
use Kirby\Cms\Site;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\A;
@@ -29,6 +30,12 @@ return [
'create' => function ($create = null) {
return $create;
},
/**
* Filters pages by a query. Sorting will be disabled
*/
'query' => function (string|null $query = null) {
return $query;
},
/**
* Filters pages by their status. Available status settings: `draft`, `unlisted`, `listed`, `published`, `all`.
*/
@@ -43,11 +50,23 @@ return [
return $status;
},
/**
* Filters the list by single template.
*/
'template' => function (string|array $template = null) {
return $template;
},
/**
* Filters the list by templates and sets template options when adding new pages to the section.
*/
'templates' => function ($templates = null) {
return A::wrap($templates ?? $this->template);
},
/**
* Excludes the selected templates.
*/
'templatesIgnore' => function ($templates = null) {
return A::wrap($templates);
}
],
'computed' => [
@@ -64,13 +83,17 @@ return [
return $parent;
},
'pages' => function () {
$pages = match ($this->status) {
'draft' => $this->parent->drafts(),
'listed' => $this->parent->children()->listed(),
'published' => $this->parent->children(),
'unlisted' => $this->parent->children()->unlisted(),
default => $this->parent->childrenAndDrafts()
};
if ($this->query !== null) {
$pages = $this->parent->query($this->query, Pages::class) ?? new Pages([]);
} else {
$pages = match ($this->status) {
'draft' => $this->parent->drafts(),
'listed' => $this->parent->children()->listed(),
'published' => $this->parent->children(),
'unlisted' => $this->parent->children()->unlisted(),
default => $this->parent->childrenAndDrafts()
};
}
// filters pages that are protected and not in the templates list
// internal `filter()` method used instead of foreach loop that previously included `unset()`
@@ -78,13 +101,26 @@ return [
// also it has been tested that there is no performance difference
// even in 0.1 seconds on 100k virtual pages
$pages = $pages->filter(function ($page) {
// remove all protected pages
if ($page->isReadable() === false) {
// remove all protected and hidden pages
if ($page->isListable() === false) {
return false;
}
$intendedTemplate = $page->intendedTemplate()->name();
// filter by all set templates
if ($this->templates && in_array($page->intendedTemplate()->name(), $this->templates) === false) {
if (
$this->templates &&
in_array($intendedTemplate, $this->templates) === false
) {
return false;
}
// exclude by all ignored templates
if (
$this->templatesIgnore &&
in_array($intendedTemplate, $this->templatesIgnore) === true
) {
return false;
}
@@ -216,6 +252,11 @@ return [
$templates = $this->kirby()->blueprints();
}
// excludes ignored templates
if ($templatesIgnore = $this->templatesIgnore) {
$templates = array_diff($templates, $templatesIgnore);
}
// convert every template to a usable option array
// for the template select box
foreach ($templates as $template) {

View File

@@ -1,11 +1,5 @@
<?php
/**
* Constants
* @deprecated 3.8.0 Use `/` instead
*/
define('DS', '/');
/**
* Class aliases
*/

View File

@@ -2,6 +2,8 @@
use Kirby\Cms\Html;
use Kirby\Cms\Url;
use Kirby\Text\KirbyTag;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuid;
@@ -15,8 +17,12 @@ return [
*/
'date' => [
'attr' => [],
'html' => function ($tag) {
return strtolower($tag->date) === 'year' ? date('Y') : date($tag->date);
'html' => function (KirbyTag $tag): string {
if (strtolower($tag->date) === 'year') {
return date('Y');
}
return date($tag->date);
}
],
@@ -31,7 +37,7 @@ return [
'text',
'title'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
return Html::email($tag->value, $tag->text, [
'class' => $tag->class,
'rel' => $tag->rel,
@@ -53,7 +59,7 @@ return [
'text',
'title'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
if (!$file = $tag->file($tag->value)) {
return $tag->text;
}
@@ -81,7 +87,7 @@ return [
'attr' => [
'file'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
return Html::gist($tag->value, $tag->file);
}
],
@@ -99,16 +105,29 @@ return [
'link',
'linkclass',
'rel',
'srcset',
'target',
'title',
'width'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
if ($tag->file = $tag->file($tag->value)) {
$tag->src = $tag->file->url();
$tag->alt = $tag->alt ?? $tag->file->alt()->or(' ')->value();
$tag->title = $tag->title ?? $tag->file->title()->value();
$tag->caption = $tag->caption ?? $tag->file->caption()->value();
$tag->src = $tag->file->url();
$tag->alt ??= $tag->file->alt()->or('')->value();
$tag->title ??= $tag->file->title()->value();
$tag->caption ??= $tag->file->caption()->value();
if ($srcset = $tag->srcset) {
$srcset = Str::split($srcset);
$srcset = match (count($srcset) > 1) {
// comma-separated list of sizes
true => A::map($srcset, fn ($size) => (int)trim($size)),
// srcset config name
default => $srcset[0]
};
$tag->srcset = $tag->file->srcset($srcset);
}
} else {
$tag->src = Url::to($tag->value);
}
@@ -129,11 +148,12 @@ return [
};
$image = Html::img($tag->src, [
'srcset' => $tag->srcset,
'width' => $tag->width,
'height' => $tag->height,
'class' => $tag->imgclass,
'title' => $tag->title,
'alt' => $tag->alt ?? ' '
'alt' => $tag->alt ?? ''
]);
if ($tag->kirby()->option('kirbytext.image.figure', true) === false) {
@@ -147,7 +167,7 @@ return [
$tag->caption = [$caption];
}
return Html::figure([ $link($image) ], $tag->caption, [
return Html::figure([$link($image)], $tag->caption, [
'class' => $tag->class
]);
}
@@ -166,7 +186,7 @@ return [
'title',
'text',
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
if (empty($tag->lang) === false) {
$tag->value = Url::to($tag->value, $tag->lang);
}
@@ -200,7 +220,7 @@ return [
'text',
'title'
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
return Html::tel($tag->value, $tag->text, [
'class' => $tag->class,
'rel' => $tag->rel,
@@ -209,37 +229,6 @@ return [
}
],
/**
* Twitter
*/
'twitter' => [
'attr' => [
'class',
'rel',
'target',
'text',
'title'
],
'html' => function ($tag) {
// get and sanitize the username
$username = str_replace('@', '', $tag->value);
// build the profile url
$url = 'https://twitter.com/' . $username;
// sanitize the link text
$text = $tag->text ?? '@' . $username;
// build the final link
return Html::a($url, $text, [
'class' => $tag->class,
'rel' => $tag->rel,
'target' => $tag->target,
'title' => $tag->title,
]);
}
],
/**
* Video
*/
@@ -258,7 +247,7 @@ return [
'style',
'width',
],
'html' => function ($tag) {
'html' => function (KirbyTag $tag): string {
// checks and gets if poster is local file
if (
empty($tag->poster) === false &&