Add blueprints and fake content

This commit is contained in:
Paul Nicoué 2021-11-18 17:44:47 +01:00
parent 1ff19bf38f
commit 8235816462
592 changed files with 22385 additions and 31535 deletions

View file

@ -3,9 +3,8 @@
namespace Kirby\Cms;
use Kirby\Api\Api as BaseApi;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Toolkit\Str;
use Kirby\Form\Form;
/**
* Api
@ -61,10 +60,12 @@ class Api extends BaseApi
{
$field = Form::for($model)->field($name);
$fieldApi = $this->clone([
'routes' => $field->api(),
'data' => array_merge($this->data(), ['field' => $field])
]);
$fieldApi = new static(
array_merge($this->propertyData, [
'data' => array_merge($this->data(), ['field' => $field]),
'routes' => $field->api(),
]),
);
return $fieldApi->call($path, $this->requestMethod(), $this->requestData());
}
@ -80,19 +81,7 @@ class Api extends BaseApi
*/
public function file(string $path = null, string $filename)
{
$filename = urldecode($filename);
$file = $this->parent($path)->file($filename);
if ($file && $file->isReadable() === true) {
return $file;
}
throw new NotFoundException([
'key' => 'file.notFound',
'data' => [
'filename' => $filename
]
]);
return Find::file($path, $filename);
}
/**
@ -105,49 +94,7 @@ class Api extends BaseApi
*/
public function parent(string $path)
{
$modelType = in_array($path, ['site', 'account']) ? $path : trim(dirname($path), '/');
$modelTypes = [
'site' => 'site',
'users' => 'user',
'pages' => 'page',
'account' => 'account'
];
$modelName = $modelTypes[$modelType] ?? null;
if (Str::endsWith($modelType, '/files') === true) {
$modelName = 'file';
}
$kirby = $this->kirby();
switch ($modelName) {
case 'site':
$model = $kirby->site();
break;
case 'account':
$model = $kirby->user(null, $kirby->option('api.allowImpersonation', false));
break;
case 'page':
$id = str_replace(['+', ' '], '/', basename($path));
$model = $kirby->page($id);
break;
case 'file':
$model = $this->file(...explode('/files/', $path));
break;
case 'user':
$model = $kirby->user(basename($path));
break;
default:
throw new InvalidArgumentException('Invalid model type: ' . $modelType);
}
if ($model) {
return $model;
}
throw new NotFoundException([
'key' => $modelName . '.undefined'
]);
return Find::parent($path);
}
/**
@ -179,19 +126,7 @@ class Api extends BaseApi
*/
public function page(string $id)
{
$id = str_replace('+', '/', $id);
$page = $this->kirby->page($id);
if ($page && $page->isReadable() === true) {
return $page;
}
throw new NotFoundException([
'key' => 'page.notFound',
'data' => [
'slug' => $id
]
]);
return Find::page($id);
}
/**
@ -287,22 +222,15 @@ class Api extends BaseApi
*/
public function user(string $id = null)
{
// get the authenticated user
if ($id === null) {
return $this->kirby->auth()->user(null, $this->kirby()->option('api.allowImpersonation', false));
}
try {
return Find::user($id);
} catch (NotFoundException $e) {
if ($id === null) {
return null;
}
// get a specific user by id
if ($user = $this->kirby->users()->find($id)) {
return $user;
throw $e;
}
throw new NotFoundException([
'key' => 'user.notFound',
'data' => [
'name' => $id
]
]);
}
/**

View file

@ -3,22 +3,23 @@
namespace Kirby\Cms;
use Kirby\Data\Data;
use Kirby\Email\PHPMailer as Emailer;
use Kirby\Exception\ErrorPageException;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Http\Request;
use Kirby\Http\Router;
use Kirby\Http\Server;
use Kirby\Http\Uri;
use Kirby\Http\Visitor;
use Kirby\Session\AutoSession;
use Kirby\Text\KirbyTag;
use Kirby\Text\KirbyTags;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Config;
use Kirby\Toolkit\Controller;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Properties;
use Throwable;
@ -53,6 +54,7 @@ class App
protected $api;
protected $collections;
protected $core;
protected $defaultLanguage;
protected $language;
protected $languages;
@ -84,6 +86,8 @@ class App
*/
public function __construct(array $props = [], bool $setInstance = true)
{
$this->core = new Core($this);
// register all roots to be able to load stuff afterwards
$this->bakeRoots($props['roots'] ?? []);
@ -265,7 +269,7 @@ class App
*/
protected function bakeRoots(array $roots = null)
{
$roots = array_merge(require dirname(__DIR__, 2) . '/config/roots.php', (array)$roots);
$roots = array_merge($this->core->roots(), (array)$roots);
$this->roots = Ingredients::bake($roots);
return $this;
}
@ -283,7 +287,7 @@ class App
$urls['index'] = $this->options['url'];
}
$urls = array_merge(require $this->root('kirby') . '/config/urls.php', (array)$urls);
$urls = array_merge($this->core->urls(), (array)$urls);
$this->urls = Ingredients::bake($urls);
return $this;
}
@ -503,6 +507,17 @@ class App
return null;
}
/**
* Get access to object that lists
* all parts of Kirby core
*
* @return \Kirby\Cms\Core
*/
public function core()
{
return $this->core;
}
/**
* Returns the default language object
*
@ -555,11 +570,14 @@ class App
*
* @param mixed $preset
* @param array $props
* @return \Kirby\Email\PHPMailer
* @return \Kirby\Email\Email
*/
public function email($preset = [], array $props = [])
{
return new Emailer((new Email($preset, $props))->toArray(), $props['debug'] ?? false);
$debug = $props['debug'] ?? false;
$props = (new Email($preset, $props))->toArray();
return ($this->component('email'))($this, $props, $debug);
}
/**
@ -641,12 +659,11 @@ class App
// any direct exception will be turned into an error page
if (is_a($input, 'Throwable') === true) {
if (is_a($input, 'Kirby\Exception\Exception') === true) {
$code = $input->getHttpCode();
$message = $input->getMessage();
$code = $input->getHttpCode();
} else {
$code = $input->getCode();
$message = $input->getMessage();
$code = $input->getCode();
}
$message = $input->getMessage();
if ($code < 400 || $code > 599) {
$code = 500;
@ -748,8 +765,13 @@ class App
$data['kirby'] = $data['kirby'] ?? $this;
$data['site'] = $data['site'] ?? $data['kirby']->site();
$data['parent'] = $data['parent'] ?? $data['site']->page();
$options = $this->options;
return KirbyTags::parse($text, $data, $this->options, $this);
$text = $this->apply('kirbytags:before', compact('text', 'data', 'options'), 'text');
$text = KirbyTags::parse($text, $data, $options);
$text = $this->apply('kirbytags:after', compact('text', 'data', 'options'), 'text');
return $text;
}
/**
@ -818,17 +840,28 @@ class App
/**
* Returns all available site languages
*
* @param bool
* @return \Kirby\Cms\Languages
*/
public function languages()
public function languages(bool $clone = true)
{
if ($this->languages !== null) {
return clone $this->languages;
return $clone === true ? clone $this->languages : $this->languages;
}
return $this->languages = Languages::load();
}
/**
* Access Kirby's part loader
*
* @return \Kirby\Cms\Loader
*/
public function load()
{
return new Loader($this);
}
/**
* Returns the app's locks object
*
@ -996,6 +1029,10 @@ class App
$parent = $parent ?? $this->site();
if ($page = $parent->find($id)) {
/**
* We passed a single $id, we can be sure that the result is
* @var \Kirby\Cms\Page $page
*/
return $page;
}
@ -1213,7 +1250,7 @@ class App
}
$registry = $this->extensions('routes');
$system = (include $this->root('kirby') . '/config/routes.php')($this);
$system = $this->core->routes();
$routes = array_merge($system['before'], $registry, $system['after']);
return $this->routes = $routes;

View file

@ -53,7 +53,7 @@ trait AppCaches
// initialize the cache class
$cache = new $className($options);
// check if it is a useable cache object
// check if it is a usable cache object
if (is_a($cache, 'Kirby\Cache\Cache') !== true) {
throw new InvalidArgumentException([
'key' => 'app.invalid.cacheType',

View file

@ -4,12 +4,15 @@ namespace Kirby\Cms;
use Closure;
use Kirby\Exception\DuplicateException;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Filesystem\Mime;
use Kirby\Form\Field as FormField;
use Kirby\Image\Image;
use Kirby\Panel\Panel;
use Kirby\Text\KirbyTag;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Collection as ToolkitCollection;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\V;
/**
@ -30,13 +33,6 @@ trait AppPlugins
*/
protected static $plugins = [];
/**
* Cache for system extensions
*
* @var array
*/
protected static $systemExtensions = null;
/**
* The extension registry
*
@ -48,7 +44,11 @@ trait AppPlugins
// other plugin types
'api' => [],
'areas' => [],
'authChallenges' => [],
'blockMethods' => [],
'blockModels' => [],
'blocksMethods' => [],
'blueprints' => [],
'cacheTypes' => [],
'collections' => [],
@ -58,9 +58,13 @@ trait AppPlugins
'collectionMethods' => [],
'fieldMethods' => [],
'fileMethods' => [],
'fileTypes' => [],
'filesMethods' => [],
'fields' => [],
'hooks' => [],
'layoutMethods' => [],
'layoutColumnMethods' => [],
'layoutsMethods' => [],
'pages' => [],
'pageMethods' => [],
'pagesMethods' => [],
@ -77,7 +81,7 @@ trait AppPlugins
'userMethods' => [],
'userModels' => [],
'usersMethods' => [],
'validators' => []
'validators' => [],
];
/**
@ -126,6 +130,25 @@ trait AppPlugins
}
}
/**
* Registers additional custom Panel areas
*
* @param array $areas
* @return array
*/
protected function extendAreas(array $areas): array
{
foreach ($areas as $id => $area) {
if (isset($this->extensions['areas'][$id]) === false) {
$this->extensions['areas'][$id] = [];
}
$this->extensions['areas'][$id][] = $area;
}
return $this->extensions['areas'];
}
/**
* Registers additional authentication challenges
*
@ -137,6 +160,39 @@ trait AppPlugins
return $this->extensions['authChallenges'] = Auth::$challenges = array_merge(Auth::$challenges, $challenges);
}
/**
* Registers additional block methods
*
* @param array $methods
* @return array
*/
protected function extendBlockMethods(array $methods): array
{
return $this->extensions['blockMethods'] = Block::$methods = array_merge(Block::$methods, $methods);
}
/**
* Registers additional block models
*
* @param array $models
* @return array
*/
protected function extendBlockModels(array $models): array
{
return $this->extensions['blockModels'] = Block::$models = array_merge(Block::$models, $models);
}
/**
* Registers additional blocks methods
*
* @param array $methods
* @return array
*/
protected function extendBlocksMethods(array $methods): array
{
return $this->extensions['blockMethods'] = Blocks::$methods = array_merge(Blocks::$methods, $methods);
}
/**
* Registers additional blueprints
*
@ -225,6 +281,59 @@ trait AppPlugins
return $this->extensions['fileMethods'] = File::$methods = array_merge(File::$methods, $methods);
}
/**
* Registers additional custom file types and mimes
*
* @param array $fileTypes
* @return array
*/
protected function extendFileTypes(array $fileTypes): array
{
// normalize array
foreach ($fileTypes as $ext => $file) {
$extension = $file['extension'] ?? $ext;
$type = $file['type'] ?? null;
$mime = $file['mime'] ?? null;
$resizable = $file['resizable'] ?? false;
$viewable = $file['viewable'] ?? false;
if (is_string($type) === true) {
if (isset(F::$types[$type]) === false) {
F::$types[$type] = [];
}
if (in_array($extension, F::$types[$type]) === false) {
F::$types[$type][] = $extension;
}
}
if ($mime !== null) {
if (array_key_exists($extension, Mime::$types) === true) {
// if `Mime::$types[$extension]` is not already an array, make it one
// and append the new MIME type unless it's already in the list
Mime::$types[$extension] = array_unique(array_merge((array)Mime::$types[$extension], (array)$mime));
} else {
Mime::$types[$extension] = $mime;
}
}
if ($resizable === true && in_array($extension, Image::$resizableTypes) === false) {
Image::$resizableTypes[] = $extension;
}
if ($viewable === true && in_array($extension, Image::$viewableTypes) === false) {
Image::$viewableTypes[] = $extension;
}
}
return $this->extensions['fileTypes'] = [
'type' => F::$types,
'mime' => Mime::$types,
'resizable' => Image::$resizableTypes,
'viewable' => Image::$viewableTypes
];
}
/**
* Registers additional files methods
*
@ -294,6 +403,39 @@ trait AppPlugins
return $this->extensions['markdown'] = $markdown;
}
/**
* Registers additional layout methods
*
* @param array $methods
* @return array
*/
protected function extendLayoutMethods(array $methods): array
{
return $this->extensions['layoutMethods'] = Layout::$methods = array_merge(Layout::$methods, $methods);
}
/**
* Registers additional layout column methods
*
* @param array $methods
* @return array
*/
protected function extendLayoutColumnMethods(array $methods): array
{
return $this->extensions['layoutColumnMethods'] = LayoutColumn::$methods = array_merge(LayoutColumn::$methods, $methods);
}
/**
* Registers additional layouts methods
*
* @param array $methods
* @return array
*/
protected function extendLayoutsMethods(array $methods): array
{
return $this->extensions['layoutsMethods'] = Layouts::$methods = array_merge(Layouts::$methods, $methods);
}
/**
* Registers additional options
*
@ -625,95 +767,27 @@ trait AppPlugins
*/
protected function extensionsFromSystem()
{
$root = $this->root('kirby');
// mixins
FormField::$mixins = $this->core->fieldMixins();
Section::$mixins = $this->core->sectionMixins();
// load static extensions only once
if (static::$systemExtensions === null) {
// Form Field Mixins
FormField::$mixins['datetime'] = include $root . '/config/fields/mixins/datetime.php';
FormField::$mixins['filepicker'] = include $root . '/config/fields/mixins/filepicker.php';
FormField::$mixins['min'] = include $root . '/config/fields/mixins/min.php';
FormField::$mixins['options'] = include $root . '/config/fields/mixins/options.php';
FormField::$mixins['pagepicker'] = include $root . '/config/fields/mixins/pagepicker.php';
FormField::$mixins['picker'] = include $root . '/config/fields/mixins/picker.php';
FormField::$mixins['upload'] = include $root . '/config/fields/mixins/upload.php';
FormField::$mixins['userpicker'] = include $root . '/config/fields/mixins/userpicker.php';
// aliases
KirbyTag::$aliases = $this->core->kirbyTagAliases();
Field::$aliases = $this->core->fieldMethodAliases();
// Tag Aliases
KirbyTag::$aliases = [
'youtube' => 'video',
'vimeo' => 'video'
];
// blueprint presets
PageBlueprint::$presets = $this->core->blueprintPresets();
// Field method aliases
Field::$aliases = [
'bool' => 'toBool',
'esc' => 'escape',
'excerpt' => 'toExcerpt',
'float' => 'toFloat',
'h' => 'html',
'int' => 'toInt',
'kt' => 'kirbytext',
'kti' => 'kirbytextinline',
'link' => 'toLink',
'md' => 'markdown',
'sp' => 'smartypants',
'v' => 'isValid',
'x' => 'xml'
];
// blueprint presets
PageBlueprint::$presets['pages'] = include $root . '/config/presets/pages.php';
PageBlueprint::$presets['page'] = include $root . '/config/presets/page.php';
PageBlueprint::$presets['files'] = include $root . '/config/presets/files.php';
// section mixins
Section::$mixins['empty'] = include $root . '/config/sections/mixins/empty.php';
Section::$mixins['headline'] = include $root . '/config/sections/mixins/headline.php';
Section::$mixins['help'] = include $root . '/config/sections/mixins/help.php';
Section::$mixins['layout'] = include $root . '/config/sections/mixins/layout.php';
Section::$mixins['max'] = include $root . '/config/sections/mixins/max.php';
Section::$mixins['min'] = include $root . '/config/sections/mixins/min.php';
Section::$mixins['pagination'] = include $root . '/config/sections/mixins/pagination.php';
Section::$mixins['parent'] = include $root . '/config/sections/mixins/parent.php';
// section types
Section::$types['info'] = include $root . '/config/sections/info.php';
Section::$types['pages'] = include $root . '/config/sections/pages.php';
Section::$types['files'] = include $root . '/config/sections/files.php';
Section::$types['fields'] = include $root . '/config/sections/fields.php';
static::$systemExtensions = [
'components' => include $root . '/config/components.php',
'blueprints' => include $root . '/config/blueprints.php',
'fields' => include $root . '/config/fields.php',
'fieldMethods' => include $root . '/config/methods.php',
'snippets' => include $root . '/config/snippets.php',
'tags' => include $root . '/config/tags.php',
'templates' => include $root . '/config/templates.php'
];
}
// default auth challenges
$this->extendAuthChallenges([
'email' => 'Kirby\Cms\Auth\EmailChallenge'
]);
// default cache types
$this->extendCacheTypes([
'apcu' => 'Kirby\Cache\ApcuCache',
'file' => 'Kirby\Cache\FileCache',
'memcached' => 'Kirby\Cache\MemCached',
'memory' => 'Kirby\Cache\MemoryCache',
]);
$this->extendComponents(static::$systemExtensions['components']);
$this->extendBlueprints(static::$systemExtensions['blueprints']);
$this->extendFields(static::$systemExtensions['fields']);
$this->extendFieldMethods((static::$systemExtensions['fieldMethods'])($this));
$this->extendSnippets(static::$systemExtensions['snippets']);
$this->extendTags(static::$systemExtensions['tags']);
$this->extendTemplates(static::$systemExtensions['templates']);
$this->extendAuthChallenges($this->core->authChallenges());
$this->extendCacheTypes($this->core->cacheTypes());
$this->extendComponents($this->core->components());
$this->extendBlueprints($this->core->blueprints());
$this->extendFields($this->core->fields());
$this->extendFieldMethods($this->core->fieldMethods());
$this->extendSections($this->core->sections());
$this->extendSnippets($this->core->snippets());
$this->extendTags($this->core->kirbyTags());
$this->extendTemplates($this->core->templates());
}
/**
@ -725,7 +799,7 @@ trait AppPlugins
*/
public function nativeComponent(string $component)
{
return static::$systemExtensions['components'][$component] ?? false;
return $this->core->components()[$component] ?? false;
}
/**

View file

@ -159,20 +159,23 @@ trait AppTranslations
* Set locale settings
*
* @deprecated 3.5.0 Use `\Kirby\Toolkit\Locale::set()` instead
* @todo Remove in 3.6.0
* @todo Remove in 3.7.0
*
* @param string|array $locale
*/
public function setLocale($locale): void
{
// @codeCoverageIgnoreStart
deprecated('`Kirby\Cms\App::setLocale()` has been deprecated and will be removed in 3.7.0. Use `Kirby\Toolkit\Locale::set()` instead');
Locale::set($locale);
// @codeCoverageIgnoreEnd
}
/**
* Load a specific translation by locale
*
* @param string|null $locale Locale name or `null` for the current locale
* @return \Kirby\Cms\Translation|null
* @return \Kirby\Cms\Translation
*/
public function translation(?string $locale = null)
{

View file

@ -1,126 +0,0 @@
<?php
namespace Kirby\Cms;
use Kirby\Toolkit\Properties;
/**
* Anything in your public path can be converted
* to an Asset object to use the same handy file
* methods and thumbnail generation as for any other
* Kirby files. Pass a relative path to the Asset
* object to create the asset.
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Asset
{
use FileFoundation;
use FileModifications;
use Properties;
/**
* @var string
*/
protected $path;
/**
* Creates a new Asset object
* for the given path.
*
* @param string $path
*/
public function __construct(string $path)
{
$this->setPath(dirname($path));
$this->setRoot($this->kirby()->root('index') . '/' . $path);
$this->setUrl($this->kirby()->url('index') . '/' . $path);
}
/**
* Returns the alternative text for the asset
*
* @return null
*/
public function alt()
{
return null;
}
/**
* Returns a unique id for the asset
*
* @return string
*/
public function id(): string
{
return $this->root();
}
/**
* Create a unique media hash
*
* @return string
*/
public function mediaHash(): string
{
return crc32($this->filename()) . '-' . $this->modified();
}
/**
* Returns the relative path starting at the media folder
*
* @return string
*/
public function mediaPath(): string
{
return 'assets/' . $this->path() . '/' . $this->mediaHash() . '/' . $this->filename();
}
/**
* Returns the absolute path to the file in the public media folder
*
* @return string
*/
public function mediaRoot(): string
{
return $this->kirby()->root('media') . '/' . $this->mediaPath();
}
/**
* Returns the absolute Url to the file in the public media folder
*
* @return string
*/
public function mediaUrl(): string
{
return $this->kirby()->url('media') . '/' . $this->mediaPath();
}
/**
* Returns the path of the file from the web root,
* excluding the filename
*
* @return string
*/
public function path(): string
{
return $this->path;
}
/**
* Setter for the path
*
* @param string $path
* @return $this
*/
protected function setPath(string $path)
{
$this->path = $path === '.' ? '' : $path;
return $this;
}
}

View file

@ -8,10 +8,10 @@ use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Exception\NotFoundException;
use Kirby\Exception\PermissionException;
use Kirby\Filesystem\F;
use Kirby\Http\Idn;
use Kirby\Http\Request\Auth\BasicAuth;
use Kirby\Toolkit\A;
use Kirby\Toolkit\F;
use Throwable;
/**
@ -95,21 +95,7 @@ class Auth
*/
public function createChallenge(string $email, bool $long = false, string $mode = 'login')
{
// ensure that email addresses with IDN domains are in Unicode format
$email = Idn::decodeEmail($email);
if ($this->isBlocked($email) === true) {
$this->kirby->trigger('user.login:failed', compact('email'));
if ($this->kirby->option('debug') === true) {
$message = 'Rate limit exceeded';
} else {
// avoid leaking security-relevant information
$message = ['key' => 'access.login'];
}
throw new PermissionException($message);
}
$email = $this->validateEmail($email);
// rate-limit the number of challenges for DoS/DDoS protection
$this->track($email, false);
@ -190,7 +176,7 @@ class Auth
$fromHeader = $this->kirby->request()->csrf();
// check for a predefined csrf or use the one from session
$fromSession = $this->kirby->option('api.csrf', csrf());
$fromSession = $this->csrfFromSession();
// compare both tokens
if (hash_equals((string)$fromSession, (string)$fromHeader) !== true) {
@ -200,6 +186,18 @@ class Auth
return $fromSession;
}
/**
* Returns either predefined csrf or the one from session
* @since 3.6.0
*
* @return string
*/
public function csrfFromSession(): string
{
$isDev = $this->kirby->option('panel.dev', false) !== false;
return $this->kirby->option('api.csrf', $isDev ? 'dev' : csrf());
}
/**
* Returns the logged in user by checking
* for a basic authentication header with
@ -384,7 +382,7 @@ class Auth
* @param bool $long
* @return \Kirby\Cms\User
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occured with debug mode off
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/
@ -415,7 +413,7 @@ class Auth
* @param bool $long
* @return \Kirby\Cms\Auth\Status
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occured with debug mode off
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/
@ -493,18 +491,15 @@ class Auth
}
/**
* Validates the user credentials and returns the user object on success;
* otherwise logs the failed attempt
* Ensures that email addresses with IDN domains are in Unicode format
* and that the rate limit was not exceeded
*
* @param string $email
* @param string $password
* @return \Kirby\Cms\User
* @return string The normalized Unicode email address
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occured with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded
*/
public function validatePassword(string $email, string $password)
protected function validateEmail(string $email): string
{
// ensure that email addresses with IDN domains are in Unicode format
$email = Idn::decodeEmail($email);
@ -523,6 +518,25 @@ class Auth
throw new PermissionException($message);
}
return $email;
}
/**
* Validates the user credentials and returns the user object on success;
* otherwise logs the failed attempt
*
* @param string $email
* @param string $password
* @return \Kirby\Cms\User
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/
public function validatePassword(string $email, string $password)
{
$email = $this->validateEmail($email);
// validate the user
try {
if ($user = $this->kirby->users()->find($email)) {
@ -724,7 +738,7 @@ class Auth
* logged in user will be returned
* @return \Kirby\Cms\User|null
*
* @throws \Throwable If an authentication error occured
* @throws \Throwable If an authentication error occurred
*/
public function user($session = null, bool $allowImpersonation = true)
{
@ -770,7 +784,7 @@ class Auth
* @return \Kirby\Cms\User User object of the logged-in user
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded, the challenge timed out, the code
* is incorrect or if any other error occured with debug mode off
* is incorrect or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the user from the challenge doesn't exist
* @throws \Kirby\Exception\InvalidArgumentException If no authentication challenge is active
* @throws \Kirby\Exception\LogicException If the authentication challenge is invalid
@ -830,7 +844,7 @@ class Auth
throw new LogicException('Invalid authentication challenge: ' . $challenge);
} catch (Throwable $e) {
if ($e->getMessage() !== 'Rate limit exceeded') {
if (empty($email) === false && $e->getMessage() !== 'Rate limit exceeded') {
$this->track($email);
}

View file

@ -66,6 +66,7 @@ class EmailChallenge extends Challenge
'template' => 'auth/' . $mode,
'data' => [
'user' => $user,
'site' => $kirby->system()->title(),
'code' => $formatted,
'timeout' => round($options['timeout'] / 60)
]

View file

@ -22,6 +22,8 @@ class Block extends Item
{
const ITEMS_CLASS = '\Kirby\Cms\Blocks';
use HasMethods;
/**
* @var \Kirby\Cms\Content
*/
@ -32,6 +34,13 @@ class Block extends Item
*/
protected $isHidden;
/**
* Registry with all block models
*
* @var array
*/
public static $models = [];
/**
* @var string
*/
@ -46,6 +55,11 @@ class Block extends Item
*/
public function __call(string $method, array $args = [])
{
// block methods
if ($this->hasMethod($method)) {
return $this->callMethod($method, $args);
}
return $this->content()->get($method);
}
@ -53,7 +67,7 @@ class Block extends Item
* Creates a new block object
*
* @param array $params
* @param \Kirby\Cms\Blocks $siblings
* @throws \Kirby\Exception\InvalidArgumentException
*/
public function __construct(array $params)
{
@ -69,7 +83,7 @@ class Block extends Item
$this->content = $params['content'] ?? [];
$this->isHidden = $params['isHidden'] ?? false;
$this->type = $params['type'] ?? null;
$this->type = $params['type'];
// create the content object
$this->content = new Content($this->content, $this->parent);
@ -89,13 +103,13 @@ class Block extends Item
* Deprecated method to return the block type
*
* @deprecated 3.5.0 Use `\Kirby\Cms\Block::type()` instead
* @todo Add deprecated() helper warning in 3.6.0
* @todo Remove in 3.7.0
*
* @return string
*/
public function _key(): string
{
deprecated('Block::_key() has been deprecated. Use Block::type() instead.');
return $this->type();
}
@ -103,13 +117,13 @@ class Block extends Item
* Deprecated method to return the block id
*
* @deprecated 3.5.0 Use `\Kirby\Cms\Block::id()` instead
* @todo Add deprecated() helper warning in 3.6.0
* @todo Remove in 3.7.0
*
* @return string
*/
public function _uid(): string
{
deprecated('Block::_uid() has been deprecated. Use Block::id() instead.');
return $this->id();
}
@ -154,6 +168,38 @@ class Block extends Item
return Str::excerpt($this->toHtml(), ...$args);
}
/**
* Constructs a block object with registering blocks models
*
* @param array $params
* @return static
* @throws \Kirby\Exception\InvalidArgumentException
* @internal
*/
public static function factory(array $params)
{
$type = $params['type'] ?? null;
if (empty($type) === false && $class = (static::$models[$type] ?? null)) {
$object = new $class($params);
if (is_a($object, 'Kirby\Cms\Block') === true) {
return $object;
}
}
// default model for blocks
if ($class = (static::$models['Kirby\Cms\Block'] ?? null)) {
$object = new $class($params);
if (is_a($object, 'Kirby\Cms\Block') === true) {
return $object;
}
}
return new static($params);
}
/**
* Checks if the block is empty
*

View file

@ -167,6 +167,14 @@ class BlockConverter
return static::editorHeading($params, 'h6');
}
public static function editorHr(array $params): array
{
return [
'content' => [],
'type' => 'line'
];
}
public static function editorHeading(array $params, string $level): array
{
return [

View file

@ -99,6 +99,18 @@ class Blocks extends Items
return $blocks;
}
/**
* Checks if a given block type exists in the collection
* @since 3.6.0
*
* @param string $type
* @return bool
*/
public function hasType(string $type): bool
{
return $this->filterBy('type', $type)->count() > 0;
}
/**
* Parse and sanitize various block formats
*

View file

@ -6,9 +6,9 @@ use Exception;
use Kirby\Data\Data;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\F;
use Kirby\Form\Field;
use Kirby\Toolkit\A;
use Kirby\Toolkit\F;
use Kirby\Toolkit\I18n;
use Throwable;
@ -58,6 +58,10 @@ class Blueprint
throw new InvalidArgumentException('A blueprint model is required');
}
if (is_a($props['model'], ModelWithContent::class) === false) {
throw new InvalidArgumentException('Invalid blueprint model');
}
$this->model = $props['model'];
// the model should not be included in the props array
@ -288,6 +292,8 @@ class Blueprint
return static::$loaded[$name] = Data::read($file);
} elseif (is_array($file) === true) {
return static::$loaded[$name] = $file;
} elseif (is_callable($file) === true) {
return static::$loaded[$name] = $file($kirby);
}
// neither a valid file nor array data
@ -697,6 +703,7 @@ class Blueprint
'columns' => $this->normalizeColumns($tabName, $tabProps['columns'] ?? []),
'icon' => $tabProps['icon'] ?? null,
'label' => $this->i18n($tabProps['label'] ?? ucfirst($tabName)),
'link' => $this->model->panel()->url(true) . '/?tab=' . $tabName,
'name' => $tabName,
]);
}
@ -720,7 +727,13 @@ class Blueprint
return $props;
}
return static::$presets[$props['preset']]($props);
$preset = static::$presets[$props['preset']];
if (is_string($preset) === true) {
$preset = require $preset;
}
return $preset($props);
}
/**
@ -760,11 +773,15 @@ class Blueprint
/**
* Returns a single tab by name
*
* @param string $name
* @param string|null $name
* @return array|null
*/
public function tab(string $name): ?array
public function tab(?string $name = null): ?array
{
if ($name === null) {
return A::first($this->tabs);
}
return $this->tabs[$name] ?? null;
}

View file

@ -165,16 +165,16 @@ class Collection extends BaseCollection
* Checks if the given object or id
* is in the collection
*
* @param string|object $id
* @param string|object $key
* @return bool
*/
public function has($id): bool
public function has($key): bool
{
if (is_object($id) === true) {
$id = $id->id();
if (is_object($key) === true) {
$key = $key->id();
}
return parent::has($id);
return parent::has($key);
}
/**
@ -182,16 +182,16 @@ class Collection extends BaseCollection
* The method will automatically detect objects
* or ids and then search accordingly.
*
* @param string|object $object
* @param string|object $needle
* @return int
*/
public function indexOf($object): int
public function indexOf($needle): int
{
if (is_string($object) === true) {
return array_search($object, $this->keys());
if (is_string($needle) === true) {
return array_search($needle, $this->keys());
}
return array_search($object->id(), $this->keys());
return array_search($needle->id(), $this->keys());
}
/**
@ -270,17 +270,17 @@ class Collection extends BaseCollection
* offset, limit, search and paginate on the collection.
* Any part of the query is optional.
*
* @param array $query
* @param array $arguments
* @return static
*/
public function query(array $query = [])
public function query(array $arguments = [])
{
$paginate = $query['paginate'] ?? null;
$search = $query['search'] ?? null;
$paginate = $arguments['paginate'] ?? null;
$search = $arguments['search'] ?? null;
unset($query['paginate']);
unset($arguments['paginate']);
$result = parent::query($query);
$result = parent::query($arguments);
if (empty($search) === false) {
if (is_array($search) === true) {

View file

@ -3,8 +3,8 @@
namespace Kirby\Cms;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Controller;
use Kirby\Toolkit\F;
/**
* Manages and loads all collections

View file

@ -2,6 +2,8 @@
namespace Kirby\Cms;
use Kirby\Form\Form;
/**
* The Content class handles all fields
* for content from pages, the site and users

View file

@ -4,7 +4,7 @@ namespace Kirby\Cms;
use Kirby\Data\Data;
use Kirby\Exception\Exception;
use Kirby\Toolkit\F;
use Kirby\Filesystem\F;
/**
* Manages all content lock files

550
kirby/src/Cms/Core.php Normal file
View file

@ -0,0 +1,550 @@
<?php
namespace Kirby\Cms;
/**
* The Core class lists all parts of Kirby
* that need to be loaded or initalized in order
* to make the system work. Most core parts can
* be overwritten by plugins.
*
* You can get such lists as kirbytags, components,
* areas, etc. by accessing them through `$kirby->core()`
*
* I.e. `$kirby->core()->areas()`
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Core
{
/**
* @var array
*/
protected $cache = [];
/**
* @var \Kirby\Cms\App
*/
protected $kirby;
/**
* @var string
*/
protected $root;
/**
* @param \Kirby\Cms\App $kirby
*/
public function __construct(App $kirby)
{
$this->kirby = $kirby;
$this->root = dirname(__DIR__, 2) . '/config';
}
/**
* Fetches the definition array of a particular area.
*
* This is a shortcut for `$kirby->core()->load()->area()`
* to give faster access to original area code in plugins.
*
* @param string $name
* @return array|null
*/
public function area(string $name): ?array
{
return $this->load()->area($name);
}
/**
* Returns a list of all paths to area definition files
*
* They are located in `/kirby/config/areas`
*
* @return array
*/
public function areas(): array
{
return [
'account' => $this->root . '/areas/account.php',
'installation' => $this->root . '/areas/installation.php',
'languages' => $this->root . '/areas/languages.php',
'login' => $this->root . '/areas/login.php',
'site' => $this->root . '/areas/site.php',
'system' => $this->root . '/areas/system.php',
'users' => $this->root . '/areas/users.php',
];
}
/**
* Returns a list of all default auth challenge classes
*
* @return array
*/
public function authChallenges(): array
{
return [
'email' => 'Kirby\Cms\Auth\EmailChallenge'
];
}
/**
* Returns a list of all paths to blueprint presets
*
* They are located in `/kirby/config/presets`
*
* @return array
*/
public function blueprintPresets(): array
{
return [
'pages' => $this->root . '/presets/pages.php',
'page' => $this->root . '/presets/page.php',
'files' => $this->root . '/presets/files.php',
];
}
/**
* Returns a list of all paths to core blueprints
*
* They are located in `/kirby/config/blueprints`.
* Block blueprints are located in `/kirby/config/blocks`
*
* @return array
*/
public function blueprints(): array
{
return [
// blocks
'blocks/code' => $this->root . '/blocks/code/code.yml',
'blocks/gallery' => $this->root . '/blocks/gallery/gallery.yml',
'blocks/heading' => $this->root . '/blocks/heading/heading.yml',
'blocks/image' => $this->root . '/blocks/image/image.yml',
'blocks/line' => $this->root . '/blocks/line/line.yml',
'blocks/list' => $this->root . '/blocks/list/list.yml',
'blocks/markdown' => $this->root . '/blocks/markdown/markdown.yml',
'blocks/quote' => $this->root . '/blocks/quote/quote.yml',
'blocks/table' => $this->root . '/blocks/table/table.yml',
'blocks/text' => $this->root . '/blocks/text/text.yml',
'blocks/video' => $this->root . '/blocks/video/video.yml',
// file blueprints
'files/default' => $this->root . '/blueprints/files/default.yml',
// page blueprints
'pages/default' => $this->root . '/blueprints/pages/default.yml',
// site blueprints
'site' => $this->root . '/blueprints/site.yml'
];
}
/**
* Returns a list of all cache driver classes
*
* @return array
*/
public function cacheTypes(): array
{
return [
'apcu' => 'Kirby\Cache\ApcuCache',
'file' => 'Kirby\Cache\FileCache',
'memcached' => 'Kirby\Cache\MemCached',
'memory' => 'Kirby\Cache\MemoryCache',
];
}
/**
* Returns an array of all core component functions
*
* The component functions can be found in
* `/kirby/config/components.php`
*
* @return array
*/
public function components(): array
{
return $this->cache['components'] ?? $this->cache['components'] = include $this->root . '/components.php';
}
/**
* Returns a map of all field method aliases
*
* @return array
*/
public function fieldMethodAliases(): array
{
return [
'bool' => 'toBool',
'esc' => 'escape',
'excerpt' => 'toExcerpt',
'float' => 'toFloat',
'h' => 'html',
'int' => 'toInt',
'kt' => 'kirbytext',
'kti' => 'kirbytextinline',
'link' => 'toLink',
'md' => 'markdown',
'sp' => 'smartypants',
'v' => 'isValid',
'x' => 'xml'
];
}
/**
* Returns an array of all field method functions
*
* Field methods are stored in `/kirby/config/methods.php`
*
* @return array
*/
public function fieldMethods(): array
{
return $this->cache['fieldMethods'] ?? $this->cache['fieldMethods'] = (include $this->root . '/methods.php')($this->kirby);
}
/**
* Returns an array of paths for field mixins
*
* They are located in `/kirby/config/fields/mixins`
*
* @return array
*/
public function fieldMixins(): array
{
return [
'datetime' => $this->root . '/fields/mixins/datetime.php',
'filepicker' => $this->root . '/fields/mixins/filepicker.php',
'layout' => $this->root . '/fields/mixins/layout.php',
'min' => $this->root . '/fields/mixins/min.php',
'options' => $this->root . '/fields/mixins/options.php',
'pagepicker' => $this->root . '/fields/mixins/pagepicker.php',
'picker' => $this->root . '/fields/mixins/picker.php',
'upload' => $this->root . '/fields/mixins/upload.php',
'userpicker' => $this->root . '/fields/mixins/userpicker.php',
];
}
/**
* Returns an array of all paths and class names of panel fields
*
* Traditional panel fields are located in `/kirby/config/fields`
*
* The more complex field classes can be found in
* `/kirby/src/Form/Fields`
*
* @return array
*/
public function fields(): array
{
return [
'blocks' => 'Kirby\Form\Field\BlocksField',
'checkboxes' => $this->root . '/fields/checkboxes.php',
'date' => $this->root . '/fields/date.php',
'email' => $this->root . '/fields/email.php',
'files' => $this->root . '/fields/files.php',
'gap' => $this->root . '/fields/gap.php',
'headline' => $this->root . '/fields/headline.php',
'hidden' => $this->root . '/fields/hidden.php',
'info' => $this->root . '/fields/info.php',
'layout' => 'Kirby\Form\Field\LayoutField',
'line' => $this->root . '/fields/line.php',
'list' => $this->root . '/fields/list.php',
'multiselect' => $this->root . '/fields/multiselect.php',
'number' => $this->root . '/fields/number.php',
'pages' => $this->root . '/fields/pages.php',
'radio' => $this->root . '/fields/radio.php',
'range' => $this->root . '/fields/range.php',
'select' => $this->root . '/fields/select.php',
'slug' => $this->root . '/fields/slug.php',
'structure' => $this->root . '/fields/structure.php',
'tags' => $this->root . '/fields/tags.php',
'tel' => $this->root . '/fields/tel.php',
'text' => $this->root . '/fields/text.php',
'textarea' => $this->root . '/fields/textarea.php',
'time' => $this->root . '/fields/time.php',
'toggle' => $this->root . '/fields/toggle.php',
'url' => $this->root . '/fields/url.php',
'users' => $this->root . '/fields/users.php',
'writer' => $this->root . '/fields/writer.php'
];
}
/**
* Returns a map of all kirbytag aliases
*
* @return array
*/
public function kirbyTagAliases(): array
{
return [
'youtube' => 'video',
'vimeo' => 'video'
];
}
/**
* Returns an array of all kirbytag definitions
*
* They are located in `/kirby/config/tags.php`
*
* @return array
*/
public function kirbyTags(): array
{
return $this->cache['kirbytags'] ?? $this->cache['kirbytags'] = include $this->root . '/tags.php';
}
/**
* Loads a core part of Kirby
*
* The loader is set to not include plugins.
* This way, you can access original Kirby core code
* through this load method.
*
* @return \Kirby\Cms\Loader
*/
public function load()
{
return new Loader($this->kirby, false);
}
/**
* Returns all absolute paths to important directories
*
* Roots are resolved and baked in `\Kirby\Cms\App::bakeRoots()`
*
* @return array
*/
public function roots(): array
{
return $this->cache['roots'] ?? $this->cache['roots'] = [
// kirby
'kirby' => function (array $roots) {
return dirname(__DIR__, 2);
},
// i18n
'i18n' => function (array $roots) {
return $roots['kirby'] . '/i18n';
},
'i18n:translations' => function (array $roots) {
return $roots['i18n'] . '/translations';
},
'i18n:rules' => function (array $roots) {
return $roots['i18n'] . '/rules';
},
// index
'index' => function (array $roots) {
return dirname(__DIR__, 3);
},
// assets
'assets' => function (array $roots) {
return $roots['index'] . '/assets';
},
// content
'content' => function (array $roots) {
return $roots['index'] . '/content';
},
// media
'media' => function (array $roots) {
return $roots['index'] . '/media';
},
// panel
'panel' => function (array $roots) {
return $roots['kirby'] . '/panel';
},
// site
'site' => function (array $roots) {
return $roots['index'] . '/site';
},
'accounts' => function (array $roots) {
return $roots['site'] . '/accounts';
},
'blueprints' => function (array $roots) {
return $roots['site'] . '/blueprints';
},
'cache' => function (array $roots) {
return $roots['site'] . '/cache';
},
'collections' => function (array $roots) {
return $roots['site'] . '/collections';
},
'config' => function (array $roots) {
return $roots['site'] . '/config';
},
'controllers' => function (array $roots) {
return $roots['site'] . '/controllers';
},
'languages' => function (array $roots) {
return $roots['site'] . '/languages';
},
'license' => function (array $roots) {
return $roots['config'] . '/.license';
},
'logs' => function (array $roots) {
return $roots['site'] . '/logs';
},
'models' => function (array $roots) {
return $roots['site'] . '/models';
},
'plugins' => function (array $roots) {
return $roots['site'] . '/plugins';
},
'sessions' => function (array $roots) {
return $roots['site'] . '/sessions';
},
'snippets' => function (array $roots) {
return $roots['site'] . '/snippets';
},
'templates' => function (array $roots) {
return $roots['site'] . '/templates';
},
// blueprints
'roles' => function (array $roots) {
return $roots['blueprints'] . '/users';
},
];
}
/**
* Returns an array of all routes for Kirbys router
*
* Routes are split into `before` and `after` routes.
*
* Plugin routes will be injected inbetween.
*
* @return array
*/
public function routes(): array
{
return $this->cache['routes'] ?? $this->cache['routes'] = (include $this->root . '/routes.php')($this->kirby);
}
/**
* Returns a list of all paths to core block snippets
*
* They are located in `/kirby/config/blocks`
*
* @return array
*/
public function snippets(): array
{
return [
'blocks/code' => $this->root . '/blocks/code/code.php',
'blocks/gallery' => $this->root . '/blocks/gallery/gallery.php',
'blocks/heading' => $this->root . '/blocks/heading/heading.php',
'blocks/image' => $this->root . '/blocks/image/image.php',
'blocks/line' => $this->root . '/blocks/line/line.php',
'blocks/list' => $this->root . '/blocks/list/list.php',
'blocks/markdown' => $this->root . '/blocks/markdown/markdown.php',
'blocks/quote' => $this->root . '/blocks/quote/quote.php',
'blocks/table' => $this->root . '/blocks/table/table.php',
'blocks/text' => $this->root . '/blocks/text/text.php',
'blocks/video' => $this->root . '/blocks/video/video.php',
];
}
/**
* Returns a list of paths to section mixins
*
* They are located in `/kirby/config/sections/mixins`
*
* @return array
*/
public function sectionMixins(): array
{
return [
'empty' => $this->root . '/sections/mixins/empty.php',
'headline' => $this->root . '/sections/mixins/headline.php',
'help' => $this->root . '/sections/mixins/help.php',
'layout' => $this->root . '/sections/mixins/layout.php',
'max' => $this->root . '/sections/mixins/max.php',
'min' => $this->root . '/sections/mixins/min.php',
'pagination' => $this->root . '/sections/mixins/pagination.php',
'parent' => $this->root . '/sections/mixins/parent.php',
];
}
/**
* Returns a list of all section definitions
*
* They are located in `/kirby/config/sections`
*
* @return array
*/
public function sections(): array
{
return [
'fields' => $this->root . '/sections/fields.php',
'files' => $this->root . '/sections/files.php',
'info' => $this->root . '/sections/info.php',
'pages' => $this->root . '/sections/pages.php',
];
}
/**
* Returns a list of paths to all system templates
*
* They are located in `/kirby/config/templates`
*
* @return array
*/
public function templates(): array
{
return [
'emails/auth/login' => $this->root . '/templates/emails/auth/login.php',
'emails/auth/password-reset' => $this->root . '/templates/emails/auth/password-reset.php'
];
}
/**
* Returns an array with all system URLs
*
* URLs are resolved and baked in `\Kirby\Cms\App::bakeUrls()`
*
* @return array
*/
public function urls(): array
{
return $this->cache['urls'] ?? $this->cache['urls'] = [
'index' => function () {
return Url::index();
},
'base' => function (array $urls) {
return rtrim($urls['index'], '/');
},
'current' => function (array $urls) {
$path = trim($this->kirby->path(), '/');
if (empty($path) === true) {
return $urls['index'];
} else {
return $urls['base'] . '/' . $path;
}
},
'assets' => function (array $urls) {
return $urls['base'] . '/assets';
},
'api' => function (array $urls) {
return $urls['base'] . '/' . $this->kirby->option('api.slug', 'api');
},
'media' => function (array $urls) {
return $urls['base'] . '/media';
},
'panel' => function (array $urls) {
return $urls['base'] . '/' . $this->kirby->option('panel.slug', 'panel');
}
];
}
}

View file

@ -1,180 +0,0 @@
<?php
namespace Kirby\Cms;
/**
* Extension of the Toolkit `Dir` class with a new
* `Dir::inventory` method, that handles scanning directories
* and converts the results into our children, files and
* other page stuff.
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Dir extends \Kirby\Toolkit\Dir
{
public static $numSeparator = '_';
/**
* Scans the directory and analyzes files,
* content, meta info and children. This is used
* in Page, Site and User objects to fetch all
* relevant information.
*
* @param string $dir
* @param string $contentExtension
* @param array|null $contentIgnore
* @param bool $multilang
* @return array
*/
public static function inventory(string $dir, string $contentExtension = 'txt', array $contentIgnore = null, bool $multilang = false): array
{
$dir = realpath($dir);
$inventory = [
'children' => [],
'files' => [],
'template' => 'default',
];
if ($dir === false) {
return $inventory;
}
$items = Dir::read($dir, $contentIgnore);
// a temporary store for all content files
$content = [];
// sort all items naturally to avoid sorting issues later
natsort($items);
foreach ($items as $item) {
// ignore all items with a leading dot
if (in_array(substr($item, 0, 1), ['.', '_']) === true) {
continue;
}
$root = $dir . '/' . $item;
if (is_dir($root) === true) {
// extract the slug and num of the directory
if (preg_match('/^([0-9]+)' . static::$numSeparator . '(.*)$/', $item, $match)) {
$num = $match[1];
$slug = $match[2];
} else {
$num = null;
$slug = $item;
}
$inventory['children'][] = [
'dirname' => $item,
'model' => null,
'num' => $num,
'root' => $root,
'slug' => $slug,
];
} else {
$extension = pathinfo($item, PATHINFO_EXTENSION);
switch ($extension) {
case 'htm':
case 'html':
case 'php':
// don't track those files
break;
case $contentExtension:
$content[] = pathinfo($item, PATHINFO_FILENAME);
break;
default:
$inventory['files'][$item] = [
'filename' => $item,
'extension' => $extension,
'root' => $root,
];
}
}
}
// remove the language codes from all content filenames
if ($multilang === true) {
foreach ($content as $key => $filename) {
$content[$key] = pathinfo($filename, PATHINFO_FILENAME);
}
$content = array_unique($content);
}
$inventory = static::inventoryContent($inventory, $content);
$inventory = static::inventoryModels($inventory, $contentExtension, $multilang);
return $inventory;
}
/**
* Take all content files,
* remove those who are meta files and
* detect the main content file
*
* @param array $inventory
* @param array $content
* @return array
*/
protected static function inventoryContent(array $inventory, array $content): array
{
// filter meta files from the content file
if (empty($content) === true) {
$inventory['template'] = 'default';
return $inventory;
}
foreach ($content as $contentName) {
// could be a meta file. i.e. cover.jpg
if (isset($inventory['files'][$contentName]) === true) {
continue;
}
// it's most likely the template
$inventory['template'] = $contentName;
}
return $inventory;
}
/**
* Go through all inventory children
* and inject a model for each
*
* @param array $inventory
* @param string $contentExtension
* @param bool $multilang
* @return array
*/
protected static function inventoryModels(array $inventory, string $contentExtension, bool $multilang = false): array
{
// inject models
if (empty($inventory['children']) === false && empty(Page::$models) === false) {
if ($multilang === true) {
$contentExtension = App::instance()->defaultLanguage()->code() . '.' . $contentExtension;
}
foreach ($inventory['children'] as $key => $child) {
foreach (Page::$models as $modelName => $modelClass) {
if (file_exists($child['root'] . '/' . $modelName . '.' . $contentExtension) === true) {
$inventory['children'][$key]['model'] = $modelName;
break;
}
}
}
}
return $inventory;
}
}

View file

@ -75,14 +75,14 @@ class Field
$method = strtolower($method);
if (isset(static::$methods[$method]) === true) {
return static::$methods[$method](clone $this, ...$arguments);
return (static::$methods[$method])(clone $this, ...$arguments);
}
if (isset(static::$aliases[$method]) === true) {
$method = strtolower(static::$aliases[$method]);
if (isset(static::$methods[$method]) === true) {
return static::$methods[$method](clone $this, ...$arguments);
return (static::$methods[$method])(clone $this, ...$arguments);
}
}

View file

@ -3,6 +3,7 @@
namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Form\Form;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
@ -21,6 +22,7 @@ class Fieldset extends Item
const ITEMS_CLASS = '\Kirby\Cms\Fieldsets';
protected $disabled;
protected $editable;
protected $fields = [];
protected $icon;
protected $label;
@ -49,6 +51,7 @@ class Fieldset extends Item
parent::__construct($params);
$this->disabled = $params['disabled'] ?? false;
$this->editable = $params['editable'] ?? true;
$this->icon = $params['icon'] ?? null;
$this->model = $this->parent;
$this->name = $this->createName($params['name'] ?? Str::ucfirst($this->type));
@ -70,6 +73,10 @@ class Fieldset extends Item
}
}
/**
* @param array $fields
* @return array
*/
protected function createFields(array $fields = []): array
{
$fields = Blueprint::fieldsProps($fields);
@ -81,16 +88,28 @@ class Fieldset extends Item
return $fields;
}
protected function createName($name): string
/**
* @param array|string $name
* @return string|null
*/
protected function createName($name): ?string
{
return I18n::translate($name, $name);
}
/**
* @param array|string $label
* @return string|null
*/
protected function createLabel($label = null): ?string
{
return I18n::translate($label, $label);
}
/**
* @param array $params
* @return array
*/
protected function createTabs(array $params = []): array
{
$tabs = $params['tabs'] ?? [];
@ -124,11 +143,33 @@ class Fieldset extends Item
return $tabs;
}
/**
* @return bool
*/
public function disabled(): bool
{
return $this->disabled;
}
/**
* @return bool
*/
public function editable(): bool
{
if ($this->editable === false) {
return false;
}
if (count($this->fields) === 0) {
return false;
}
return true;
}
/**
* @return array
*/
public function fields(): array
{
return $this->fields;
@ -139,7 +180,7 @@ class Fieldset extends Item
*
* @param array $fields
* @param array $input
* @return \Kirby\Cms\Form
* @return \Kirby\Form\Form
*/
public function form(array $fields, array $input = [])
{
@ -151,36 +192,65 @@ class Fieldset extends Item
]);
}
/**
* @return string|null
*/
public function icon(): ?string
{
return $this->icon;
}
/**
* @return string|null
*/
public function label(): ?string
{
return $this->label;
}
/**
* @return \Kirby\Cms\ModelWithContent
*/
public function model()
{
return $this->model;
}
/**
* @return string
*/
public function name(): string
{
return $this->name;
}
/**
* @return string|bool
*/
public function preview()
{
return $this->preview;
}
/**
* @return array
*/
public function tabs(): array
{
return $this->tabs;
}
/**
* @return bool
*/
public function translate(): bool
{
return $this->translate;
}
/**
* @return string
*/
public function type(): string
{
return $this->type;
@ -192,21 +262,33 @@ class Fieldset extends Item
public function toArray(): array
{
return [
'disabled' => $this->disabled,
'icon' => $this->icon,
'label' => $this->label,
'name' => $this->name,
'preview' => $this->preview,
'tabs' => $this->tabs,
'translate' => $this->translate,
'type' => $this->type,
'unset' => $this->unset,
'wysiwyg' => $this->wysiwyg,
'disabled' => $this->disabled(),
'editable' => $this->editable(),
'icon' => $this->icon(),
'label' => $this->label(),
'name' => $this->name(),
'preview' => $this->preview(),
'tabs' => $this->tabs(),
'translate' => $this->translate(),
'type' => $this->type(),
'unset' => $this->unset(),
'wysiwyg' => $this->wysiwyg(),
];
}
/**
* @return bool
*/
public function unset(): bool
{
return $this->unset;
}
/**
* @return bool
*/
public function wysiwyg(): bool
{
return $this->wysiwyg;
}
}

View file

@ -74,6 +74,7 @@ class Fieldsets extends Items
'gallery' => 'blocks/gallery',
'heading' => 'blocks/heading',
'image' => 'blocks/image',
'line' => 'blocks/line',
'list' => 'blocks/list',
'markdown' => 'blocks/markdown',
'quote' => 'blocks/quote',

View file

@ -2,11 +2,10 @@
namespace Kirby\Cms;
use Kirby\Image\Image;
use Kirby\Filesystem\F;
use Kirby\Filesystem\IsFile;
use Kirby\Panel\File as Panel;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\F;
use Throwable;
/**
* The `$file` object provides a set
@ -16,11 +15,11 @@ use Throwable;
* URL or resizing an image. It also
* handles file meta data.
*
* The File class is a wrapper around
* the Kirby\Image\Image class, which
* is used to handle all file methods.
* The File class proxies the `Kirby\Filesystem\File`
* or `Kirby\Image\Image` class, which
* is used to handle all asset file methods.
* In addition the File class handles
* File meta data via Kirby\Cms\Content.
* meta data via `Kirby\Cms\Content`.
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
@ -33,19 +32,10 @@ class File extends ModelWithContent
const CLASS_ALIAS = 'file';
use FileActions;
use FileFoundation;
use FileModifications;
use HasMethods;
use HasSiblings;
/**
* The parent asset object
* This is used to do actual file
* method calls, like size, mime, etc.
*
* @var \Kirby\Image\Image
*/
protected $asset;
use IsFile;
/**
* Cache for the initialized blueprint object
@ -57,12 +47,12 @@ class File extends ModelWithContent
/**
* @var string
*/
protected $id;
protected $filename;
/**
* @var string
*/
protected $filename;
protected $id;
/**
* All registered file methods
@ -123,7 +113,7 @@ class File extends ModelWithContent
}
// content fields
return $this->content()->get($method, $arguments);
return $this->content()->get($method);
}
/**
@ -133,7 +123,11 @@ class File extends ModelWithContent
*/
public function __construct(array $props)
{
// properties
// set filename as the most important prop first
// TODO: refactor later to avoid redundant prop setting
$this->setProperty('filename', $props['filename'] ?? null, true);
// set other properties
$this->setProperties($props);
}
@ -162,17 +156,6 @@ class File extends ModelWithContent
return $this->parent()->apiUrl($relative) . '/files/' . $this->filename();
}
/**
* Returns the Image object
*
* @internal
* @return \Kirby\Image\Image
*/
public function asset()
{
return $this->asset = $this->asset ?? new Image($this->root());
}
/**
* Returns the FileBlueprint object for the file
*
@ -226,43 +209,6 @@ class File extends ModelWithContent
return $this->filename();
}
/**
* Provides a kirbytag or markdown
* tag for the file, which will be
* used in the panel, when the file
* gets dragged onto a textarea
*
* @internal
* @param string|null $type (null|auto|kirbytext|markdown)
* @param bool $absolute
* @return string
*/
public function dragText(string $type = null, bool $absolute = false): string
{
$type = $this->dragTextType($type);
$url = $absolute ? $this->id() : $this->filename();
if ($dragTextFromCallback = $this->dragTextFromCallback($type, $url)) {
return $dragTextFromCallback;
}
if ($type === 'markdown') {
if ($this->type() === 'image') {
return '![' . $this->alt() . '](' . $url . ')';
} else {
return '[' . $this->filename() . '](' . $url . ')';
}
} else {
if ($this->type() === 'image') {
return '(image: ' . $url . ')';
} elseif ($this->type() === 'video') {
return '(video: ' . $url . ')';
} else {
return '(file: ' . $url . ')';
}
}
}
/**
* Constructs a File object
*
@ -295,6 +241,20 @@ class File extends ModelWithContent
return $this->siblingsCollection();
}
/**
* Converts the file to html
*
* @param array $attr
* @return string
*/
public function html(array $attr = []): string
{
return $this->asset()->html(array_merge(
['alt' => $this->alt()],
$attr
));
}
/**
* Returns the id
*
@ -446,156 +406,13 @@ class File extends ModelWithContent
}
/**
* Panel icon definition
* Returns the panel info object
*
* @internal
* @param array|null $params
* @return array
* @return \Kirby\Panel\File
*/
public function panelIcon(array $params = null): array
public function panel()
{
$colorBlue = '#81a2be';
$colorPurple = '#b294bb';
$colorOrange = '#de935f';
$colorGreen = '#a7bd68';
$colorAqua = '#8abeb7';
$colorYellow = '#f0c674';
$colorRed = '#d16464';
$colorWhite = '#c5c9c6';
$types = [
'image' => ['color' => $colorOrange, 'type' => 'file-image'],
'video' => ['color' => $colorYellow, 'type' => 'file-video'],
'document' => ['color' => $colorRed, 'type' => 'file-document'],
'audio' => ['color' => $colorAqua, 'type' => 'file-audio'],
'code' => ['color' => $colorBlue, 'type' => 'file-code'],
'archive' => ['color' => $colorWhite, 'type' => 'file-zip'],
];
$extensions = [
'indd' => ['color' => $colorPurple],
'xls' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'],
'xlsx' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'],
'csv' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'],
'docx' => ['color' => $colorBlue, 'type' => 'file-word'],
'doc' => ['color' => $colorBlue, 'type' => 'file-word'],
'rtf' => ['color' => $colorBlue, 'type' => 'file-word'],
'mdown' => ['type' => 'file-text'],
'md' => ['type' => 'file-text']
];
$definition = array_merge($types[$this->type()] ?? [], $extensions[$this->extension()] ?? []);
$params['type'] = $definition['type'] ?? 'file';
$params['color'] = $definition['color'] ?? $colorWhite;
return parent::panelIcon($params);
}
/**
* Returns the image file object based on provided query
*
* @internal
* @param string|null $query
* @return \Kirby\Cms\File|\Kirby\Cms\Asset|null
*/
protected function panelImageSource(string $query = null)
{
if ($query === null && $this->isViewable()) {
return $this;
}
return parent::panelImageSource($query);
}
/**
* Returns an array of all actions
* that can be performed in the Panel
*
* @since 3.3.0 This also checks for the lock status
* @since 3.5.1 This also checks for matching accept settings
*
* @param array $unlock An array of options that will be force-unlocked
* @return array
*/
public function panelOptions(array $unlock = []): array
{
$options = parent::panelOptions($unlock);
try {
// check if the file type is allowed at all,
// otherwise it cannot be replaced
$this->match($this->blueprint()->accept());
} catch (Throwable $e) {
$options['replace'] = false;
}
return $options;
}
/**
* Returns the full path without leading slash
*
* @internal
* @return string
*/
public function panelPath(): string
{
return 'files/' . $this->filename();
}
/**
* Prepares the response data for file pickers
* and file fields
*
* @param array|null $params
* @return array
*/
public function panelPickerData(array $params = []): array
{
$image = $this->panelImage($params['image'] ?? []);
$icon = $this->panelIcon($image);
$uuid = $this->id();
if (empty($params['model']) === false) {
$uuid = $this->parent() === $params['model'] ? $this->filename() : $this->id();
$absolute = $this->parent() !== $params['model'];
}
// escape the default text
// TODO: no longer needed in 3.6
$textQuery = $params['text'] ?? '{{ file.filename }}';
$text = $this->toString($textQuery);
if ($textQuery === '{{ file.filename }}') {
$text = Escape::html($text);
}
return [
'filename' => $this->filename(),
'dragText' => $this->dragText('auto', $absolute ?? false),
'icon' => $icon,
'id' => $this->id(),
'image' => $image,
'info' => $this->toString($params['info'] ?? false),
'link' => $this->panelUrl(true),
'text' => $text,
'type' => $this->type(),
'url' => $this->url(),
'uuid' => $uuid,
];
}
/**
* Returns the url to the editing view
* in the panel
*
* @internal
* @param bool $relative
* @return string
*/
public function panelUrl(bool $relative = false): string
{
return $this->parent()->panelUrl($relative) . '/' . $this->panelPath();
return new Panel($this);
}
/**
@ -612,6 +429,7 @@ class File extends ModelWithContent
* Returns the parent id if a parent exists
*
* @internal
* @todo 3.7.0 When setParent() is changed, the if check is not needed anymore
* @return string|null
*/
public function parentId(): ?string
@ -697,13 +515,22 @@ class File extends ModelWithContent
}
/**
* Sets the parent model object
* Sets the parent model object;
* this property is required for `File::create()` and
* will be generally required starting with Kirby 3.7.0
*
* @param \Kirby\Cms\Model|null $parent
* @return $this
* @todo make property required in 3.7.0
*/
protected function setParent(Model $parent = null)
{
// @codeCoverageIgnoreStart
if ($parent === null) {
deprecated('You are creating a `Kirby\Cms\File` object without passing the `parent` property. While unsupported, this hasn\'t caused any direct errors so far. To fix inconsistencies, the `parent` property will be required when creating a `Kirby\Cms\File` object in Kirby 3.7.0 and higher. Not passing this property will start throwing a breaking error.');
}
// @codeCoverageIgnoreEnd
$this->parent = $parent;
return $this;
}
@ -806,4 +633,134 @@ class File extends ModelWithContent
{
return $this->url ?? $this->url = ($this->kirby()->component('file::url'))($this->kirby(), $this);
}
/**
* Deprecated!
*/
/**
* Provides a kirbytag or markdown
* tag for the file, which will be
* used in the panel, when the file
* gets dragged onto a textarea
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @param string|null $type (null|auto|kirbytext|markdown)
* @param bool $absolute
* @return string
* @codeCoverageIgnore
*/
public function dragText(string $type = null, bool $absolute = false): string
{
return $this->panel()->dragText($type, $absolute);
}
/**
* Returns an array of all actions
* that can be performed in the Panel
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @since 3.3.0 This also checks for the lock status
* @since 3.5.1 This also checks for matching accept settings
*
* @param array $unlock An array of options that will be force-unlocked
* @return array
* @codeCoverageIgnore
*/
public function panelOptions(array $unlock = []): array
{
return $this->panel()->options($unlock);
}
/**
* Returns the full path without leading slash
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @return string
* @codeCoverageIgnore
*/
public function panelPath(): string
{
return $this->panel()->path();
}
/**
* Prepares the response data for file pickers
* and file fields
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @param array|null $params
* @return array
* @codeCoverageIgnore
*/
public function panelPickerData(array $params = []): array
{
return $this->panel()->pickerData($params);
}
/**
* Returns the url to the editing view
* in the panel
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @param bool $relative
* @return string
* @codeCoverageIgnore
*/
public function panelUrl(bool $relative = false): string
{
return $this->panel()->url($relative);
}
/**
* Simplified File URL that uses the parent
* Page URL and the filename as a more stable
* alternative for the media URLs.
*
* @return string
*/
public function previewUrl(): string
{
$parent = $this->parent();
$url = url($this->id());
switch ($parent::CLASS_ALIAS) {
case 'page':
$preview = $parent->blueprint()->preview();
// the page has a custom preview setting,
// thus the file is only accessible through
// the direct media URL
if ($preview !== true) {
return $this->url();
}
// it's more stable to access files for drafts
// through their direct URL to avoid conflicts
// with draft token verification
if ($parent->isDraft() === true) {
return $this->url();
}
return $url;
case 'user':
return $this->url();
default:
return $url;
}
}
}

View file

@ -5,8 +5,8 @@ namespace Kirby\Cms;
use Closure;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Image\Image;
use Kirby\Toolkit\F;
use Kirby\Filesystem\F;
use Kirby\Form\Form;
/**
* FileActions
@ -75,6 +75,8 @@ trait FileActions
F::move($oldFile->contentFile(), $newFile->contentFile());
}
$newFile->parent()->files()->remove($oldFile->id());
$newFile->parent()->files()->set($newFile->id(), $newFile);
return $newFile;
});
@ -178,7 +180,7 @@ trait FileActions
// create the basic file and a test upload object
$file = static::factory($props);
$upload = new Image($props['source']);
$upload = $file->asset($props['source']);
// create a form for the file
$form = Form::for($file, [
@ -277,7 +279,14 @@ trait FileActions
*/
public function replace(string $source)
{
return $this->commit('replace', ['file' => $this, 'upload' => new Image($source)], function ($file, $upload) {
$file = $this->clone();
$arguments = [
'file' => $file,
'upload' => $file->asset($source)
];
return $this->commit('replace', $arguments, function ($file, $upload) {
// delete all public versions
$file->unpublish();

View file

@ -2,7 +2,7 @@
namespace Kirby\Cms;
use Kirby\Toolkit\F;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Str;
/**
@ -81,7 +81,7 @@ class FileBlueprint extends Blueprint
if (is_array($accept['extension']) === true) {
// determine the main MIME type for each extension
$restrictions[] = array_map(['Kirby\Toolkit\Mime', 'fromExtension'], $accept['extension']);
$restrictions[] = array_map(['Kirby\Filesystem\Mime', 'fromExtension'], $accept['extension']);
}
if (is_array($accept['type']) === true) {
@ -89,7 +89,7 @@ class FileBlueprint extends Blueprint
$mimes = [];
foreach ($accept['type'] as $type) {
if ($extensions = F::typeToExtensions($type)) {
$mimes[] = array_map(['Kirby\Toolkit\Mime', 'fromExtension'], $extensions);
$mimes[] = array_map(['Kirby\Filesystem\Mime', 'fromExtension'], $extensions);
}
}

View file

@ -1,248 +0,0 @@
<?php
namespace Kirby\Cms;
use Kirby\Exception\BadMethodCallException;
use Kirby\Image\Image;
use Kirby\Toolkit\F;
/**
* Foundation for all file objects
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
trait FileFoundation
{
protected $asset;
protected $root;
protected $url;
/**
* Magic caller for asset methods
*
* @param string $method
* @param array $arguments
* @return mixed
* @throws \Kirby\Exception\BadMethodCallException
*/
public function __call(string $method, array $arguments = [])
{
// public property access
if (isset($this->$method) === true) {
return $this->$method;
}
// asset method proxy
if (method_exists($this->asset(), $method)) {
return $this->asset()->$method(...$arguments);
}
throw new BadMethodCallException('The method: "' . $method . '" does not exist');
}
/**
* Constructor sets all file properties
*
* @param array $props
*/
public function __construct(array $props)
{
$this->setProperties($props);
}
/**
* Converts the file object to a string
* In case of an image, it will create an image tag
* Otherwise it will return the url
*
* @return string
*/
public function __toString(): string
{
if ($this->type() === 'image') {
return $this->html();
}
return $this->url();
}
/**
* Returns the Image object
*
* @return \Kirby\Image\Image
*/
public function asset()
{
return $this->asset = $this->asset ?? new Image($this->root());
}
/**
* Checks if the file exists on disk
*
* @return bool
*/
public function exists(): bool
{
return file_exists($this->root()) === true;
}
/**
* Returns the file extension
*
* @return string
*/
public function extension(): string
{
return F::extension($this->root());
}
/**
* Converts the file to html
*
* @param array $attr
* @return string
*/
public function html(array $attr = []): string
{
if ($this->type() === 'image') {
return Html::img($this->url(), array_merge(['alt' => $this->alt()], $attr));
} else {
return Html::a($this->url(), $attr);
}
}
/**
* Checks if the file is a resizable image
*
* @return bool
*/
public function isResizable(): bool
{
$resizable = [
'jpg',
'jpeg',
'gif',
'png',
'webp'
];
return in_array($this->extension(), $resizable) === true;
}
/**
* Checks if a preview can be displayed for the file
* in the panel or in the frontend
*
* @return bool
*/
public function isViewable(): bool
{
$viewable = [
'jpg',
'jpeg',
'gif',
'png',
'svg',
'webp'
];
return in_array($this->extension(), $viewable) === true;
}
/**
* Returns the app instance
*
* @return \Kirby\Cms\App
*/
public function kirby()
{
return App::instance();
}
/**
* Get the file's last modification time.
*
* @param string $format
* @param string|null $handler date or strftime
* @return mixed
*/
public function modified(string $format = null, string $handler = null)
{
return F::modified($this->root(), $format, $handler ?? $this->kirby()->option('date.handler', 'date'));
}
/**
* Returns the absolute path to the file root
*
* @return string|null
*/
public function root(): ?string
{
return $this->root;
}
/**
* Setter for the root
*
* @param string|null $root
* @return $this
*/
protected function setRoot(string $root = null)
{
$this->root = $root;
return $this;
}
/**
* Setter for the file url
*
* @param string $url
* @return $this
*/
protected function setUrl(string $url)
{
$this->url = $url;
return $this;
}
/**
* Convert the object to an array
*
* @return array
*/
public function toArray(): array
{
$array = array_merge($this->asset()->toArray(), [
'isResizable' => $this->isResizable(),
'url' => $this->url(),
]);
ksort($array);
return $array;
}
/**
* Returns the file type
*
* @return string|null
*/
public function type(): ?string
{
return F::type($this->root());
}
/**
* Returns the absolute url for the file
*
* @return string
*/
public function url(): string
{
return $this->url;
}
}

View file

@ -5,7 +5,7 @@ namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
/**
* Resizing, blurring etc.
* Trait for image resizing, blurring etc.
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
@ -191,9 +191,20 @@ trait FileModifications
return $this;
}
$result = ($this->kirby()->component('file::version'))($this->kirby(), $this, $options);
// fallback to global config options
if (isset($options['format']) === false) {
if ($format = $this->kirby()->option('thumbs.format')) {
$options['format'] = $format;
}
}
if (is_a($result, 'Kirby\Cms\FileVersion') === false && is_a($result, 'Kirby\Cms\File') === false) {
$component = $this->kirby()->component('file::version');
$result = $component($this->kirby(), $this, $options);
if (
is_a($result, 'Kirby\Cms\FileVersion') === false &&
is_a($result, 'Kirby\Cms\File') === false
) {
throw new InvalidArgumentException('The file::version component must return a File or FileVersion object');
}

View file

@ -5,7 +5,7 @@ namespace Kirby\Cms;
use Kirby\Exception\DuplicateException;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\PermissionException;
use Kirby\Image\Image;
use Kirby\Filesystem\File as BaseFile;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
@ -38,6 +38,12 @@ class FileRules
]);
}
if (Str::length($name) === 0) {
throw new InvalidArgumentException([
'key' => 'file.changeName.empty'
]);
}
$parent = $file->parent();
$duplicate = $parent->files()->not($file)->findBy('filename', $name . '.' . $file->extension());
@ -67,15 +73,22 @@ class FileRules
* Validates if the file can be created
*
* @param \Kirby\Cms\File $file
* @param \Kirby\Image\Image $upload
* @param \Kirby\Filesystem\File $upload
* @return bool
* @throws \Kirby\Exception\DuplicateException If a file with the same name exists
* @throws \Kirby\Exception\PermissionException If the user is not allowed to create the file
*/
public static function create(File $file, Image $upload): bool
public static function create(File $file, BaseFile $upload): bool
{
if ($file->exists() === true) {
throw new DuplicateException('The file exists and cannot be overwritten');
if ($file->sha1() !== $upload->sha1()) {
throw new DuplicateException([
'key' => 'file.duplicate',
'data' => [
'filename' => $file->filename()
]
]);
}
}
if ($file->permissions()->create() !== true) {
@ -110,12 +123,12 @@ class FileRules
* Validates if the file can be replaced
*
* @param \Kirby\Cms\File $file
* @param \Kirby\Image\Image $upload
* @param \Kirby\Filesystem\File $upload
* @return bool
* @throws \Kirby\Exception\PermissionException If the user is not allowed to replace the file
* @throws \Kirby\Exception\InvalidArgumentException If the file type of the new file is different
*/
public static function replace(File $file, Image $upload): bool
public static function replace(File $file, BaseFile $upload): bool
{
if ($file->permissions()->replace() !== true) {
throw new PermissionException('The file cannot be replaced');
@ -169,34 +182,38 @@ class FileRules
// make it easier to compare the extension
$extension = strtolower($extension);
if (empty($extension)) {
if (empty($extension) === true) {
throw new InvalidArgumentException([
'key' => 'file.extension.missing',
'data' => ['filename' => $file->filename()]
]);
}
if (V::in($extension, ['php', 'phar', 'html', 'htm', 'exe', App::instance()->contentExtension()])) {
throw new InvalidArgumentException([
'key' => 'file.extension.forbidden',
'data' => ['extension' => $extension]
]);
}
if (Str::contains($extension, 'php') || Str::contains($extension, 'phar')) {
if (
Str::contains($extension, 'php') !== false ||
Str::contains($extension, 'phar') !== false ||
Str::contains($extension, 'phtml') !== false
) {
throw new InvalidArgumentException([
'key' => 'file.type.forbidden',
'data' => ['type' => 'PHP']
]);
}
if (Str::contains($extension, 'htm')) {
if (Str::contains($extension, 'htm') !== false) {
throw new InvalidArgumentException([
'key' => 'file.type.forbidden',
'data' => ['type' => 'HTML']
]);
}
if (V::in($extension, ['exe', App::instance()->contentExtension()]) !== false) {
throw new InvalidArgumentException([
'key' => 'file.extension.forbidden',
'data' => ['extension' => $extension]
]);
}
return true;
}

View file

@ -2,7 +2,7 @@
namespace Kirby\Cms;
use Kirby\Toolkit\Properties;
use Kirby\Filesystem\IsFile;
/**
* FileVersion
@ -15,10 +15,7 @@ use Kirby\Toolkit\Properties;
*/
class FileVersion
{
use FileFoundation {
toArray as parentToArray;
}
use Properties;
use IsFile;
protected $modifications;
protected $original;
@ -47,8 +44,8 @@ class FileVersion
return $this->asset()->$method(...$arguments);
}
// content fields
if (is_a($this->original(), 'Kirby\Cms\File') === true) {
// content fields
return $this->original()->content()->get($method, $arguments);
}
}
@ -101,7 +98,11 @@ class FileVersion
*/
public function save()
{
$this->kirby()->thumb($this->original()->root(), $this->root(), $this->modifications());
$this->kirby()->thumb(
$this->original()->root(),
$this->root(),
$this->modifications()
);
return $this;
}
@ -132,7 +133,7 @@ class FileVersion
*/
public function toArray(): array
{
$array = array_merge($this->parentToArray(), [
$array = array_merge($this->asset()->toArray(), [
'modifications' => $this->modifications(),
]);

View file

@ -1,303 +0,0 @@
<?php
namespace Kirby\Cms;
use Kirby\Toolkit\Str;
/**
* The Filename class handles complex
* mapping of file attributes (i.e for thumbnails)
* into human readable filenames.
*
* ```php
* $filename = new Filename('some-file.jpg', '{{ name }}-{{ attributes }}.{{ extension }}', [
* 'crop' => 'top left',
* 'width' => 300,
* 'height' => 200
* 'quality' => 80
* ]);
*
* echo $filename->toString();
* // result: some-file-300x200-crop-top-left-q80.jpg
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Filename
{
/**
* List of all applicable attributes
*
* @var array
*/
protected $attributes;
/**
* The sanitized file extension
*
* @var string
*/
protected $extension;
/**
* The source original filename
*
* @var string
*/
protected $filename;
/**
* The sanitized file name
*
* @var string
*/
protected $name;
/**
* The template for the final name
*
* @var string
*/
protected $template;
/**
* Creates a new Filename object
*
* @param string $filename
* @param string $template
* @param array $attributes
*/
public function __construct(string $filename, string $template, array $attributes = [])
{
$this->filename = $filename;
$this->template = $template;
$this->attributes = $attributes;
$this->extension = $this->sanitizeExtension(pathinfo($filename, PATHINFO_EXTENSION));
$this->name = $this->sanitizeName(pathinfo($filename, PATHINFO_FILENAME));
}
/**
* Converts the entire object to a string
*
* @return string
*/
public function __toString(): string
{
return $this->toString();
}
/**
* Converts all processed attributes
* to an array. The array keys are already
* the shortened versions for the filename
*
* @return array
*/
public function attributesToArray(): array
{
$array = [
'dimensions' => implode('x', $this->dimensions()),
'crop' => $this->crop(),
'blur' => $this->blur(),
'bw' => $this->grayscale(),
'q' => $this->quality(),
];
$array = array_filter($array, function ($item) {
return $item !== null && $item !== false && $item !== '';
});
return $array;
}
/**
* Converts all processed attributes
* to a string, that can be used in the
* new filename
*
* @param string|null $prefix The prefix will be used in the filename creation
* @return string
*/
public function attributesToString(string $prefix = null): string
{
$array = $this->attributesToArray();
$result = [];
foreach ($array as $key => $value) {
if ($value === true) {
$value = '';
}
switch ($key) {
case 'dimensions':
$result[] = $value;
break;
case 'crop':
$result[] = ($value === 'center') ? null : $key . '-' . $value;
break;
default:
$result[] = $key . $value;
}
}
$result = array_filter($result);
$attributes = implode('-', $result);
if (empty($attributes) === true) {
return '';
}
return $prefix . $attributes;
}
/**
* Normalizes the blur option value
*
* @return false|int
*/
public function blur()
{
$value = $this->attributes['blur'] ?? false;
if ($value === false) {
return false;
}
return (int)$value;
}
/**
* Normalizes the crop option value
*
* @return false|string
*/
public function crop()
{
// get the crop value
$crop = $this->attributes['crop'] ?? false;
if ($crop === false) {
return false;
}
return Str::slug($crop);
}
/**
* Returns a normalized array
* with width and height values
* if available
*
* @return array
*/
public function dimensions()
{
if (empty($this->attributes['width']) === true && empty($this->attributes['height']) === true) {
return [];
}
return [
'width' => $this->attributes['width'] ?? null,
'height' => $this->attributes['height'] ?? null
];
}
/**
* Returns the sanitized extension
*
* @return string
*/
public function extension(): string
{
return $this->extension;
}
/**
* Normalizes the grayscale option value
* and also the available ways to write
* the option. You can use `grayscale`,
* `greyscale` or simply `bw`. The function
* will always return `grayscale`
*
* @return bool
*/
public function grayscale(): bool
{
// normalize options
$value = $this->attributes['grayscale'] ?? $this->attributes['greyscale'] ?? $this->attributes['bw'] ?? false;
// turn anything into boolean
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
/**
* Returns the filename without extension
*
* @return string
*/
public function name(): string
{
return $this->name;
}
/**
* Normalizes the quality option value
*
* @return false|int
*/
public function quality()
{
$value = $this->attributes['quality'] ?? false;
if ($value === false || $value === true) {
return false;
}
return (int)$value;
}
/**
* Sanitizes the file extension.
* The extension will be converted
* to lowercase and `jpeg` will be
* replaced with `jpg`
*
* @param string $extension
* @return string
*/
protected function sanitizeExtension(string $extension): string
{
$extension = strtolower($extension);
$extension = str_replace('jpeg', 'jpg', $extension);
return $extension;
}
/**
* Sanitizes the name with Kirby's
* Str::slug function
*
* @param string $name
* @return string
*/
protected function sanitizeName(string $name): string
{
return Str::slug($name);
}
/**
* Returns the converted filename as string
*
* @return string
*/
public function toString(): string
{
return Str::template($this->template, [
'name' => $this->name(),
'attributes' => $this->attributesToString('-'),
'extension' => $this->extension()
], '');
}
}

View file

@ -2,6 +2,9 @@
namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Filesystem\F;
/**
* The `$files` object extends the general
* `Collection` class and refers to a
@ -30,12 +33,13 @@ class Files extends Collection
* an entire second collection to the
* current collection
*
* @param mixed $object
* @param \Kirby\Cms\Files|\Kirby\Cms\File|string $object
* @return $this
* @throws \Kirby\Exception\InvalidArgumentException When no `File` or `Files` object or an ID of an existing file is passed
*/
public function add($object)
{
// add a page collection
// add a files collection
if (is_a($object, self::class) === true) {
$this->data = array_merge($this->data, $object->data);
@ -46,6 +50,11 @@ class Files extends Collection
// add a file object
} elseif (is_a($object, 'Kirby\Cms\File') === true) {
$this->__set($object->id(), $object);
// give a useful error message on invalid input;
// silently ignore "empty" values for compatibility with existing setups
} elseif (in_array($object, [null, false, true], true) !== true) {
throw new InvalidArgumentException('You must pass a Files or File object or an ID of an existing file to the Files collection');
}
return $this;
@ -120,6 +129,44 @@ class Files extends Collection
return $this->findById($key);
}
/**
* Returns the file size for all
* files in the collection in a
* human-readable format
* @since 3.6.0
*
* @return string
*/
public function niceSize(): string
{
return F::niceSize($this->size());
}
/**
* Returns the raw size for all
* files in the collection
* @since 3.6.0
*
* @return int
*/
public function size(): int
{
return F::size($this->values(function ($file) {
return $file->root();
}));
}
/**
* Returns the collection sorted by
* the sort number and the filename
*
* @return static
*/
public function sorted()
{
return $this->sort('sort', 'asc', 'filename', 'asc');
}
/**
* Filter all files by the given template
*

191
kirby/src/Cms/Find.php Normal file
View file

@ -0,0 +1,191 @@
<?php
namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Toolkit\Str;
/**
* The Find class is used in the API and
* the Panel to find models and parents
* based on request paths
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Find
{
/**
* Returns the file object for the given
* parent path and filename
*
* @param string|null $path Path to file's parent model
* @param string $filename Filename
* @return \Kirby\Cms\File|null
* @throws \Kirby\Exception\NotFoundException if the file cannot be found
*/
public static function file(string $path = null, string $filename)
{
$filename = urldecode($filename);
$file = static::parent($path)->file($filename);
if ($file && $file->isReadable() === true) {
return $file;
}
throw new NotFoundException([
'key' => 'file.notFound',
'data' => [
'filename' => $filename
]
]);
}
/**
* Returns the language object for the given code
*
* @param string $code Language code
* @return \Kirby\Cms\Language|null
* @throws \Kirby\Exception\NotFoundException if the language cannot be found
*/
public static function language(string $code)
{
if ($language = App::instance()->language($code)) {
return $language;
}
throw new NotFoundException([
'key' => 'language.notFound',
'data' => [
'code' => $code
]
]);
}
/**
* Returns the page object for the given id
*
* @param string $id Page's id
* @return \Kirby\Cms\Page|null
* @throws \Kirby\Exception\NotFoundException if the page cannot be found
*/
public static function page(string $id)
{
$id = str_replace(['+', ' '], '/', $id);
$page = App::instance()->page($id);
if ($page && $page->isReadable() === true) {
return $page;
}
throw new NotFoundException([
'key' => 'page.notFound',
'data' => [
'slug' => $id
]
]);
}
/**
* Returns the model's object for the given path
*
* @param string $path Path to parent model
* @return \Kirby\Cms\Model|null
* @throws \Kirby\Exception\InvalidArgumentException if the model type is invalid
* @throws \Kirby\Exception\NotFoundException if the model cannot be found
*/
public static function parent(string $path)
{
$path = trim($path, '/');
$modelType = in_array($path, ['site', 'account']) ? $path : trim(dirname($path), '/');
$modelTypes = [
'site' => 'site',
'users' => 'user',
'pages' => 'page',
'account' => 'account'
];
$modelName = $modelTypes[$modelType] ?? null;
if (Str::endsWith($modelType, '/files') === true) {
$modelName = 'file';
}
$kirby = App::instance();
switch ($modelName) {
case 'site':
$model = $kirby->site();
break;
case 'account':
$model = static::user();
break;
case 'page':
$model = static::page(basename($path));
break;
case 'file':
$model = static::file(...explode('/files/', $path));
break;
case 'user':
$model = $kirby->user(basename($path));
break;
default:
throw new InvalidArgumentException('Invalid model type: ' . $modelType);
}
if ($model) {
return $model;
}
throw new NotFoundException([
'key' => $modelName . '.undefined'
]);
}
/**
* Returns the user object for the given id or
* returns the current authenticated user if no
* id is passed
*
* @param string|null $id User's id
* @return \Kirby\Cms\User|null
* @throws \Kirby\Exception\NotFoundException if the user for the given id cannot be found
*/
public static function user(string $id = null)
{
// account is a reserved word to find the current
// user. It's used in various API and area routes.
if ($id === 'account') {
$id = null;
}
$kirby = App::instance();
// get the authenticated user
if ($id === null) {
if ($user = $kirby->user(null, $kirby->option('api.allowImpersonation', false))) {
return $user;
}
throw new NotFoundException([
'key' => 'user.undefined'
]);
}
// get a specific user by id
if ($user = $kirby->user($id)) {
return $user;
}
throw new NotFoundException([
'key' => 'user.notFound',
'data' => [
'name' => $id
]
]);
}
}

View file

@ -1,130 +0,0 @@
<?php
namespace Kirby\Cms;
use Kirby\Exception\NotFoundException;
use Kirby\Form\Form as BaseForm;
use Kirby\Toolkit\Str;
/**
* Extension of `Kirby\Form\Form` that introduces
* a Form::for method that creates a proper form
* definition for any Cms Model.
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Form extends BaseForm
{
protected $errors;
protected $fields;
protected $values = [];
/**
* Form constructor.
*
* @param array $props
*/
public function __construct(array $props)
{
$kirby = App::instance();
if ($kirby->multilang() === true) {
$fields = $props['fields'] ?? [];
$languageCode = $props['language'] ?? $kirby->language()->code();
$isDefaultLanguage = $languageCode === $kirby->defaultLanguage()->code();
foreach ($fields as $fieldName => $fieldProps) {
// switch untranslatable fields to readonly
if (($fieldProps['translate'] ?? true) === false && $isDefaultLanguage === false) {
$fields[$fieldName]['unset'] = true;
$fields[$fieldName]['disabled'] = true;
}
}
$props['fields'] = $fields;
}
parent::__construct($props);
}
/**
* Get the field object by name
* and handle nested fields correctly
*
* @param string $name
* @throws \Kirby\Exception\NotFoundException
* @return \Kirby\Form\Field
*/
public function field(string $name)
{
$form = $this;
$fieldNames = Str::split($name, '+');
$index = 0;
$count = count($fieldNames);
$field = null;
foreach ($fieldNames as $fieldName) {
$index++;
if ($field = $form->fields()->get($fieldName)) {
if ($count !== $index) {
$form = $field->form();
}
} else {
throw new NotFoundException('The field "' . $fieldName . '" could not be found');
}
}
// it can get this error only if $name is an empty string as $name = ''
if ($field === null) {
throw new NotFoundException('No field could be loaded');
}
return $field;
}
/**
* @param \Kirby\Cms\Model $model
* @param array $props
* @return static
*/
public static function for(Model $model, array $props = [])
{
// get the original model data
$original = $model->content($props['language'] ?? null)->toArray();
$values = $props['values'] ?? [];
// convert closures to values
foreach ($values as $key => $value) {
if (is_a($value, 'Closure') === true) {
$values[$key] = $value($original[$key] ?? null);
}
}
// set a few defaults
$props['values'] = array_merge($original, $values);
$props['fields'] = $props['fields'] ?? [];
$props['model'] = $model;
// search for the blueprint
if (method_exists($model, 'blueprint') === true && $blueprint = $model->blueprint()) {
$props['fields'] = $blueprint->fields();
}
$ignoreDisabled = $props['ignoreDisabled'] ?? false;
// REFACTOR: this could be more elegant
if ($ignoreDisabled === true) {
$props['fields'] = array_map(function ($field) {
$field['disabled'] = false;
return $field;
}, $props['fields']);
}
return new static($props);
}
}

View file

@ -2,6 +2,7 @@
namespace Kirby\Cms;
use Kirby\Filesystem\Dir;
use Kirby\Toolkit\Str;
/**
@ -16,21 +17,21 @@ use Kirby\Toolkit\Str;
trait HasChildren
{
/**
* The Pages collection
* The list of available published children
*
* @var \Kirby\Cms\Pages
*/
public $children;
/**
* The list of available drafts
* The list of available draft children
*
* @var \Kirby\Cms\Pages
*/
public $drafts;
/**
* Returns the Pages collection
* Returns all published children
*
* @return \Kirby\Cms\Pages
*/
@ -44,7 +45,7 @@ trait HasChildren
}
/**
* Returns all children and drafts at the same time
* Returns all published and draft children at the same time
*
* @return \Kirby\Cms\Pages
*/
@ -54,8 +55,8 @@ trait HasChildren
}
/**
* Return a list of ids for the model's
* toArray method
* Returns a list of IDs for the model's
* `toArray` method
*
* @return array
*/
@ -65,7 +66,7 @@ trait HasChildren
}
/**
* Searches for a child draft by id
* Searches for a draft child by ID
*
* @param string $path
* @return \Kirby\Cms\Page|null
@ -99,7 +100,7 @@ trait HasChildren
}
/**
* Return all drafts of the model
* Returns all draft children
*
* @return \Kirby\Cms\Pages
*/
@ -123,7 +124,7 @@ trait HasChildren
}
/**
* Finds one or multiple children by id
* Finds one or multiple published children by ID
*
* @param string ...$arguments
* @return \Kirby\Cms\Page|\Kirby\Cms\Pages|null
@ -134,7 +135,7 @@ trait HasChildren
}
/**
* Finds a single page or draft
* Finds a single published or draft child
*
* @param string $path
* @return \Kirby\Cms\Page|null
@ -145,7 +146,7 @@ trait HasChildren
}
/**
* Returns a collection of all children of children
* Returns a collection of all published children of published children
*
* @return \Kirby\Cms\Pages
*/
@ -155,7 +156,7 @@ trait HasChildren
}
/**
* Checks if the model has any children
* Checks if the model has any published children
*
* @return bool
*/
@ -165,7 +166,7 @@ trait HasChildren
}
/**
* Checks if the model has any drafts
* Checks if the model has any draft children
*
* @return bool
*/
@ -197,7 +198,7 @@ trait HasChildren
/**
* Creates a flat child index
*
* @param bool $drafts
* @param bool $drafts If set to `true`, draft children are included
* @return \Kirby\Cms\Pages
*/
public function index(bool $drafts = false)
@ -210,7 +211,7 @@ trait HasChildren
}
/**
* Sets the Children collection
* Sets the published children collection
*
* @param array|null $children
* @return $this
@ -225,7 +226,7 @@ trait HasChildren
}
/**
* Sets the Drafts collection
* Sets the draft children collection
*
* @param array|null $drafts
* @return $this

View file

@ -1,64 +0,0 @@
<?php
namespace Kirby\Cms;
/**
* Extended KirbyTag class to provide
* common helpers for tag objects
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class KirbyTag extends \Kirby\Text\KirbyTag
{
/**
* Finds a file for the given path.
* The method first searches the file
* in the current parent, if it's a page.
* Afterwards it uses Kirby's global file finder.
*
* @param string $path
* @return \Kirby\Cms\File|null
*/
public function file(string $path)
{
$parent = $this->parent();
if (
is_object($parent) === true &&
method_exists($parent, 'file') === true &&
$file = $parent->file($path)
) {
return $file;
}
if (is_a($parent, 'Kirby\Cms\File') === true && $file = $parent->page()->file($path)) {
return $file;
}
return $this->kirby()->file($path, null, true);
}
/**
* Returns the current Kirby instance
*
* @return \Kirby\Cms\App
*/
public function kirby()
{
return $this->data['kirby'] ?? App::instance();
}
/**
* Returns the parent model
*
* @return \Kirby\Cms\Model|null
*/
public function parent()
{
return $this->data['parent'];
}
}

View file

@ -1,45 +0,0 @@
<?php
namespace Kirby\Cms;
/**
* Extension of `Kirby\Text\KirbyTags` that introduces
* `kirbytags:before` and `kirbytags:after` hooks
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class KirbyTags extends \Kirby\Text\KirbyTags
{
/**
* The KirbyTag rendering class
*
* @var string
*/
protected static $tagClass = 'Kirby\Cms\KirbyTag';
/**
* @param string|null $text
* @param array $data
* @param array $options
* @param \Kirby\Cms\App|null $app
* @return string
*/
public static function parse(string $text = null, array $data = [], array $options = [], ?App $app = null): string
{
if ($app !== null) {
$text = $app->apply('kirbytags:before', compact('text', 'data', 'options'), 'text');
}
$text = parent::parse($text, $data, $options);
if ($app !== null) {
$text = $app->apply('kirbytags:after', compact('text', 'data', 'options'), 'text');
}
return $text;
}
}

View file

@ -5,7 +5,7 @@ namespace Kirby\Cms;
use Kirby\Data\Data;
use Kirby\Exception\Exception;
use Kirby\Exception\PermissionException;
use Kirby\Toolkit\F;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Locale;
use Kirby\Toolkit\Str;
use Throwable;
@ -221,6 +221,9 @@ class Language extends Model
static::converter('', $language->code());
}
// update the main languages collection in the app instance
App::instance()->languages(false)->append($language->code(), $language);
return $language;
}
@ -234,23 +237,25 @@ class Language extends Model
*/
public function delete(): bool
{
if ($this->exists() === false) {
return true;
}
$kirby = App::instance();
$languages = $kirby->languages();
$code = $this->code();
$isLast = $languages->count() === 1;
if (F::remove($this->root()) !== true) {
throw new Exception('The language could not be deleted');
}
if ($languages->count() === 1) {
return $this->converter($code, '');
if ($isLast === true) {
$this->converter($code, '');
} else {
return $this->deleteContentFiles($code);
$this->deleteContentFiles($code);
}
// get the original language collection and remove the current language
$kirby->languages(false)->remove($code);
return true;
}
/**
@ -679,6 +684,11 @@ class Language extends Model
throw new PermissionException('Please select another language to be the primary language');
}
return $updated->save();
$language = $updated->save();
// make sure the language is also updated in the Kirby language collection
App::instance()->languages(false)->set($language->code(), $language);
return $language;
}
}

View file

@ -91,7 +91,7 @@ class LanguageRouter
return $page->uri($language) . '/' . $pattern;
}, $patterns);
// reinject the pattern and the full page object
// re-inject the pattern and the full page object
$routes[$index]['pattern'] = $patterns;
$routes[$index]['page'] = $page;
} else {

View file

@ -2,7 +2,7 @@
namespace Kirby\Cms;
use Kirby\Toolkit\F;
use Kirby\Filesystem\F;
class LanguageRoutes
{

View file

@ -3,7 +3,7 @@
namespace Kirby\Cms;
use Kirby\Exception\DuplicateException;
use Kirby\Toolkit\F;
use Kirby\Filesystem\F;
/**
* A collection of all defined site languages

View file

@ -17,6 +17,8 @@ class Layout extends Item
{
const ITEMS_CLASS = '\Kirby\Cms\Layouts';
use HasMethods;
/**
* @var \Kirby\Cms\Content
*/
@ -36,6 +38,11 @@ class Layout extends Item
*/
public function __call(string $method, array $args = [])
{
// layout methods
if ($this->hasMethod($method) === true) {
return $this->callMethod($method, $args);
}
return $this->attrs()->get($method);
}

View file

@ -19,6 +19,8 @@ class LayoutColumn extends Item
{
const ITEMS_CLASS = '\Kirby\Cms\LayoutColumns';
use HasMethods;
/**
* @var \Kirby\Cms\Blocks
*/
@ -45,13 +47,33 @@ class LayoutColumn extends Item
$this->width = $params['width'] ?? '1/1';
}
/**
* Magic getter function
*
* @param string $method
* @param mixed $args
* @return mixed
*/
public function __call(string $method, $args)
{
// layout column methods
if ($this->hasMethod($method) === true) {
return $this->callMethod($method, $args);
}
}
/**
* Returns the blocks collection
*
* @param bool $includeHidden Sets whether to include hidden blocks
* @return \Kirby\Cms\Blocks
*/
public function blocks()
public function blocks(bool $includeHidden = false)
{
if ($includeHidden === false) {
return $this->blocks->filter('isHidden', false);
}
return $this->blocks;
}
@ -104,7 +126,7 @@ class LayoutColumn extends Item
public function toArray(): array
{
return [
'blocks' => $this->blocks()->toArray(),
'blocks' => $this->blocks(true)->toArray(),
'id' => $this->id(),
'width' => $this->width(),
];

View file

@ -41,6 +41,18 @@ class Layouts extends Items
return parent::factory($items, $params);
}
/**
* Checks if a given block type exists in the layouts collection
* @since 3.6.0
*
* @param string $type
* @return bool
*/
public function hasBlockType(string $type): bool
{
return $this->toBlocks()->hasType($type);
}
/**
* Parse layouts data
*
@ -63,4 +75,28 @@ class Layouts extends Items
return $input;
}
/**
* Converts layouts to blocks
* @since 3.6.0
*
* @param bool $includeHidden Sets whether to include hidden blocks
* @return \Kirby\Cms\Blocks
*/
public function toBlocks(bool $includeHidden = false)
{
$blocks = [];
if ($this->isNotEmpty() === true) {
foreach ($this->data() as $layout) {
foreach ($layout->columns() as $column) {
foreach ($column->blocks($includeHidden) as $block) {
$blocks[] = $block->toArray();
}
}
}
}
return Blocks::factory($blocks);
}
}

250
kirby/src/Cms/Loader.php Normal file
View file

@ -0,0 +1,250 @@
<?php
namespace Kirby\Cms;
use Closure;
use Kirby\Data\Data;
use Kirby\Filesystem\F;
/**
* The Loader class is an internal loader for
* core parts, like areas, components, sections, etc.
*
* It's exposed in the `$kirby->load()` and the
* `$kirby->core()->load()` methods.
*
* With `$kirby->load()` you get access to core parts
* that might be overwritten by plugins.
*
* With `$kirby->core()->load()` you get access to
* untouched core parts. This is useful if you want to
* reuse or fall back to core features in your plugins.
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Loader
{
/**
* @var \Kirby\Cms\App
*/
protected $kirby;
/**
* @var bool
*/
protected $withPlugins;
/**
* @param \Kirby\Cms\App $kirby
* @param bool $withPlugins
*/
public function __construct(App $kirby, bool $withPlugins = true)
{
$this->kirby = $kirby;
$this->withPlugins = $withPlugins;
}
/**
* Loads the area definition
*
* @param string $name
* @return array|null
*/
public function area(string $name): ?array
{
return $this->areas()[$name] ?? null;
}
/**
* Loads all areas and makes sure that plugins
* are injected properly
*
* @return array
*/
public function areas(): array
{
$areas = [];
$extensions = $this->withPlugins === true ? $this->kirby->extensions('areas') : [];
// load core areas and extend them with elements from plugins if they exist
foreach ($this->kirby->core()->areas() as $id => $area) {
$area = $this->resolveArea($area);
if (isset($extensions[$id]) === true) {
foreach ($extensions[$id] as $areaExtension) {
$extension = $this->resolveArea($areaExtension);
$area = array_replace_recursive($area, $extension);
}
unset($extensions[$id]);
}
$areas[$id] = $area;
}
// add additional areas from plugins
foreach ($extensions as $id => $areaExtensions) {
foreach ($areaExtensions as $areaExtension) {
$areas[$id] = $this->resolve($areaExtension);
}
}
return $areas;
}
/**
* Loads a core component closure
*
* @param string $name
* @return \Closure|null
*/
public function component(string $name): ?Closure
{
return $this->extension('components', $name);
}
/**
* Loads all core component closures
*
* @return array
*/
public function components(): array
{
return $this->extensions('components');
}
/**
* Loads a particular extension
*
* @param string $type
* @param string $name
* @return mixed
*/
public function extension(string $type, string $name)
{
return $this->extensions($type)[$name] ?? null;
}
/**
* Loads all defined extensions
*
* @param string $type
* @return array
*/
public function extensions(string $type): array
{
return $this->withPlugins === false ? $this->kirby->core()->$type() : $this->kirby->extensions($type);
}
/**
* The resolver takes a string, array or closure.
*
* 1.) a string is supposed to be a path to an existing file.
* The file will either be included when it's a PHP file and
* the array contents will be read. Or it will be parsed with
* the Data class to read yml or json data into an array
*
* 2.) arrays are untouched and returned
*
* 3.) closures will be called and the Kirby instance will be
* passed as first argument
*
* @param mixed $item
* @return mixed
*/
public function resolve($item)
{
if (is_string($item) === true) {
if (F::extension($item) !== 'php') {
$item = Data::read($item);
} else {
$item = require $item;
}
}
if (is_callable($item)) {
$item = $item($this->kirby);
}
return $item;
}
/**
* Calls `static::resolve()` on all items
* in the given array
*
* @param array $items
* @return array
*/
public function resolveAll(array $items): array
{
$result = [];
foreach ($items as $key => $value) {
$result[$key] = $this->resolve($value);
}
return $result;
}
/**
* Areas need a bit of special treatment
* when they are being loaded
*
* @param string|array|Closure $area
* @return array
*/
public function resolveArea($area): array
{
$area = $this->resolve($area);
$dropdowns = $area['dropdowns'] ?? [];
// convert closure dropdowns to an array definition
// otherwise they cannot be merged properly later
foreach ($dropdowns as $key => $dropdown) {
if (is_a($dropdown, 'Closure') === true) {
$area['dropdowns'][$key] = [
'options' => $dropdown
];
}
}
return $area;
}
/**
* Loads a particular section definition
*
* @param string $name
* @return array|null
*/
public function section(string $name): ?array
{
return $this->resolve($this->extension('sections', $name));
}
/**
* Loads all section defintions
*
* @return array
*/
public function sections(): array
{
return $this->resolveAll($this->extensions('sections'));
}
/**
* Returns the status flag, which shows
* if plugins are loaded as well.
*
* @return bool
*/
public function withPlugins(): bool
{
return $this->withPlugins;
}
}

View file

@ -3,8 +3,8 @@
namespace Kirby\Cms;
use Kirby\Data\Data;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Str;
use Throwable;
@ -48,8 +48,8 @@ class Media
if (Str::startsWith($hash, $file->mediaToken() . '-') === true) {
return Response::redirect($file->mediaUrl(), 307);
} else {
// don't leak the correct token
return new Response('Not Found', 'text/plain', 404);
// don't leak the correct token, render the error page
return false;
}
}

View file

@ -17,6 +17,14 @@ abstract class Model
{
use Properties;
/**
* Each model must define a CLASS_ALIAS
* which will be used in template queries.
* The CLASS_ALIAS is a short human-readable
* version of the class name. I.e. page.
*/
const CLASS_ALIAS = null;
/**
* The parent Kirby instance
*

View file

@ -5,6 +5,7 @@ namespace Kirby\Cms;
use Closure;
use Kirby\Data\Data;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Form\Form;
use Kirby\Toolkit\Str;
use Throwable;
@ -19,14 +20,6 @@ use Throwable;
*/
abstract class ModelWithContent extends Model
{
/**
* Each model must define a CLASS_ALIAS
* which will be used in template queries.
* The CLASS_ALIAS is a short human-readable
* version of the class name. I.e. page.
*/
const CLASS_ALIAS = null;
/**
* The content
*
@ -244,46 +237,6 @@ abstract class ModelWithContent extends Model
return $this->update([$field => $value]);
}
/**
* Returns the drag text from a custom callback
* if the callback is defined in the config
*
* @internal
* @param string $type markdown or kirbytext
* @param mixed ...$args
* @return string|null
*/
public function dragTextFromCallback(string $type, ...$args): ?string
{
$dragTextCallback = option('panel.' . $type . '.' . static::CLASS_ALIAS . 'DragText');
if (empty($dragTextCallback) === false && is_a($dragTextCallback, 'Closure') === true && ($dragText = $dragTextCallback($this, ...$args)) !== null) {
return $dragText;
}
return null;
}
/**
* Returns the correct drag text type
* depending on the given type or the
* configuration
*
* @internal
* @param string $type (null|auto|kirbytext|markdown)
* @return string
*/
public function dragTextType(string $type = null): string
{
$type = $type ?? 'auto';
if ($type === 'auto') {
$type = option('panel.kirbytext', true) ? 'kirbytext' : 'markdown';
}
return $type === 'markdown' ? 'markdown' : 'kirbytext';
}
/**
* Returns all content validation errors
*
@ -362,144 +315,12 @@ abstract class ModelWithContent extends Model
}
/**
* Returns the panel icon definition
* Returns the panel info of the model
* @since 3.6.0
*
* @internal
* @param array|null $params
* @return array
* @return \Kirby\Panel\Model
*/
public function panelIcon(array $params = null): array
{
$defaults = [
'type' => 'page',
'ratio' => null,
'back' => 'pattern',
'color' => '#c5c9c6',
];
return array_merge($defaults, $params ?? []);
}
/**
* @internal
* @param string|array|false|null $settings
* @return array|null
*/
public function panelImage($settings = null): ?array
{
$defaults = [
'ratio' => '3/2',
'back' => 'pattern',
'cover' => false
];
// switch the image off
if ($settings === false) {
return null;
}
if (is_string($settings) === true) {
// use defined icon in blueprint
if ($settings === 'icon') {
return [];
}
$settings = [
'query' => $settings
];
}
if ($image = $this->panelImageSource($settings['query'] ?? null)) {
// main url
$settings['url'] = $image->url();
// only create srcsets for actual File objects
if (is_a($image, 'Kirby\Cms\File') === true) {
// for cards
$settings['cards'] = [
'url' => 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw',
'srcset' => $image->srcset([
352,
864,
1408,
])
];
// for lists
$settings['list'] = [
'url' => 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw',
'srcset' => $image->srcset([
'1x' => [
'width' => 38,
'height' => 38,
'crop' => 'center'
],
'2x' => [
'width' => 76,
'height' => 76,
'crop' => 'center'
],
])
];
}
unset($settings['query']);
}
return array_merge($defaults, (array)$settings);
}
/**
* Returns the image file object based on provided query
*
* @internal
* @param string|null $query
* @return \Kirby\Cms\File|\Kirby\Cms\Asset|null
*/
protected function panelImageSource(string $query = null)
{
$image = $this->query($query ?? null);
// validate the query result
if (is_a($image, 'Kirby\Cms\File') === false && is_a($image, 'Kirby\Cms\Asset') === false) {
$image = null;
}
// fallback for files
if ($image === null && is_a($this, 'Kirby\Cms\File') === true && $this->isViewable() === true) {
$image = $this;
}
return $image;
}
/**
* Returns an array of all actions
* that can be performed in the Panel
* This also checks for the lock status
* @since 3.3.0
*
* @param array $unlock An array of options that will be force-unlocked
* @return array
*/
public function panelOptions(array $unlock = []): array
{
$options = $this->permissions()->toArray();
if ($this->isLocked()) {
foreach ($options as $key => $value) {
if (in_array($key, $unlock)) {
continue;
}
$options[$key] = false;
}
}
return $options;
}
abstract public function panel();
/**
* Must return the permissions object for the model
@ -687,24 +508,43 @@ abstract class ModelWithContent extends Model
}
/**
* String template builder
* String template builder with automatic HTML escaping
* @since 3.6.0
*
* @param string|null $template
* @param string|null $template Template string or `null` to use the model ID
* @param array $data
* @param string $fallback Fallback for tokens in the template that cannot be replaced
* @return string
*/
public function toString(string $template = null, array $data = [], string $fallback = ''): string
public function toSafeString(string $template = null, array $data = [], string $fallback = ''): string
{
return $this->toString($template, $data, $fallback, 'safeTemplate');
}
/**
* String template builder
*
* @param string|null $template Template string or `null` to use the model ID
* @param array $data
* @param string $fallback Fallback for tokens in the template that cannot be replaced
* @param string $handler For internal use
* @return string
*/
public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string
{
if ($template === null) {
return $this->id();
return $this->id() ?? '';
}
$result = Str::template($template, array_replace([
if ($handler !== 'template' && $handler !== 'safeTemplate') {
throw new InvalidArgumentException('Invalid toString handler'); // @codeCoverageIgnore
}
$result = Str::$handler($template, array_replace([
'kirby' => $this->kirby(),
'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(),
static::CLASS_ALIAS => $this
], $data), $fallback);
], $data), ['fallback' => $fallback]);
return $result;
}
@ -801,4 +641,59 @@ abstract class ModelWithContent extends Model
$this->contentFileData($data, $languageCode)
);
}
/**
* Deprecated!
*/
/**
* Returns the panel icon definition
*
* @deprecated 3.6.0 Use `->panel()->image()` instead
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @param array|null $params
* @return array|null
* @codeCoverageIgnore
*/
public function panelIcon(array $params = null): ?array
{
return $this->panel()->image($params);
}
/**
* @deprecated 3.6.0 Use `->panel()->image()` instead
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @param string|array|false|null $settings
* @return array|null
* @codeCoverageIgnore
*/
public function panelImage($settings = null): ?array
{
return $this->panel()->image($settings);
}
/**
* Returns an array of all actions
* that can be performed in the Panel
* This also checks for the lock status
*
* @deprecated 3.6.0 Use `->panel()->options()` instead
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @param array $unlock An array of options that will be force-unlocked
* @return array
* @codeCoverageIgnore
*/
public function panelOptions(array $unlock = []): array
{
return $this->panel()->options($unlock);
}
}

View file

@ -5,10 +5,11 @@ namespace Kirby\Cms;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Http\Uri;
use Kirby\Panel\Page as Panel;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\F;
/**
* The `$page` object is the heart and
@ -171,7 +172,7 @@ class Page extends ModelWithContent
}
// return page content otherwise
return $this->content()->get($method, $arguments);
return $this->content()->get($method);
}
/**
@ -214,9 +215,9 @@ class Page extends ModelWithContent
public function apiUrl(bool $relative = false): string
{
if ($relative === true) {
return 'pages/' . $this->panelId();
return 'pages/' . $this->panel()->id();
} else {
return $this->kirby()->url('api') . '/pages/' . $this->panelId();
return $this->kirby()->url('api') . '/pages/' . $this->panel()->id();
}
}
@ -335,7 +336,7 @@ class Page extends ModelWithContent
* @return array
* @throws \Kirby\Exception\InvalidArgumentException If the controller returns invalid objects for `kirby`, `site`, `pages` or `page`
*/
public function controller($data = [], $contentType = 'html'): array
public function controller(array $data = [], string $contentType = 'html'): array
{
// create the template data
$data = array_merge($data, [
@ -426,31 +427,6 @@ class Page extends ModelWithContent
}
}
/**
* Provides a kirbytag or markdown
* tag for the page, which will be
* used in the panel, when the page
* gets dragged onto a textarea
*
* @internal
* @param string|null $type (null|auto|kirbytext|markdown)
* @return string
*/
public function dragText(string $type = null): string
{
$type = $this->dragTextType($type);
if ($dragTextFromCallback = $this->dragTextFromCallback($type)) {
return $dragTextFromCallback;
}
if ($type === 'markdown') {
return '[' . $this->title() . '](' . $this->url() . ')';
} else {
return '(link: ' . $this->id() . ' text: ' . $this->title() . ')';
}
}
/**
* Checks if the page exists on disk
*
@ -929,109 +905,13 @@ class Page extends ModelWithContent
}
/**
* Returns the panel icon definition
* according to the blueprint settings
* Returns the panel info object
*
* @internal
* @param array|null $params
* @return array
* @return \Kirby\Panel\Page
*/
public function panelIcon(array $params = null): array
public function panel()
{
if ($icon = $this->blueprint()->icon()) {
$params['type'] = $icon;
}
return parent::panelIcon($params);
}
/**
* Returns the escaped Id, which is
* used in the panel to make routing work properly
*
* @internal
* @return string
*/
public function panelId(): string
{
return str_replace('/', '+', $this->id());
}
/**
* Returns the image file object based on provided query
*
* @internal
* @param string|null $query
* @return \Kirby\Cms\File|\Kirby\Cms\Asset|null
*/
protected function panelImageSource(string $query = null)
{
if ($query === null) {
$query = 'page.image';
}
return parent::panelImageSource($query);
}
/**
* Returns the full path without leading slash
*
* @internal
* @return string
*/
public function panelPath(): string
{
return 'pages/' . $this->panelId();
}
/**
* Prepares the response data for page pickers
* and page fields
*
* @param array|null $params
* @return array
*/
public function panelPickerData(array $params = []): array
{
$image = $this->panelImage($params['image'] ?? []);
$icon = $this->panelIcon($image);
// escape the default text
// TODO: no longer needed in 3.6
$textQuery = $params['text'] ?? '{{ page.title }}';
$text = $this->toString($textQuery);
if ($textQuery === '{{ page.title }}') {
$text = Escape::html($text);
}
return [
'dragText' => $this->dragText(),
'hasChildren' => $this->hasChildren(),
'icon' => $icon,
'id' => $this->id(),
'image' => $image,
'info' => $this->toString($params['info'] ?? false),
'link' => $this->panelUrl(true),
'text' => $text,
'url' => $this->url(),
];
}
/**
* Returns the url to the editing view
* in the panel
*
* @internal
* @param bool $relative
* @return string
*/
public function panelUrl(bool $relative = false): string
{
if ($relative === true) {
return '/' . $this->panelPath();
} else {
return $this->kirby()->url('panel') . '/' . $this->panelPath();
}
return new Panel($this);
}
/**
@ -1383,7 +1263,9 @@ class Page extends ModelWithContent
$languageCode = $this->kirby()->languageCode();
}
if ($translation = $this->translations()->find($languageCode)) {
$defaultLanguageCode = $this->kirby()->defaultLanguage()->code();
if ($languageCode !== $defaultLanguageCode && $translation = $this->translations()->find($languageCode)) {
return $translation->slug() ?? $this->slug;
}
}
@ -1576,4 +1458,97 @@ class Page extends ModelWithContent
return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language);
}
/**
* Deprecated!
*/
/**
* Provides a kirbytag or markdown
* tag for the page, which will be
* used in the panel, when the page
* gets dragged onto a textarea
*
* @deprecated 3.6.0 Use `->panel()->dragText()` instead
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @param string|null $type (null|auto|kirbytext|markdown)
* @return string
* @codeCoverageIgnore
*/
public function dragText(string $type = null): string
{
return $this->panel()->dragText($type);
}
/**
* Returns the escaped Id, which is
* used in the panel to make routing work properly
*
* @deprecated 3.6.0 Use `->panel()->id()` instead
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @return string
* @codeCoverageIgnore
*/
public function panelId(): string
{
return $this->panel()->id();
}
/**
* Returns the full path without leading slash
*
* @deprecated 3.6.0 Use `->panel()->path()` instead
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @return string
* @codeCoverageIgnore
*/
public function panelPath(): string
{
return $this->panel()->path();
}
/**
* Prepares the response data for page pickers
* and page fields
*
* @deprecated 3.6.0 Use `->panel()->pickerData()` instead
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @param array|null $params
* @return array
* @codeCoverageIgnore
*/
public function panelPickerData(array $params = []): array
{
return $this->panel()->pickerData($params);
}
/**
* Returns the url to the editing view
* in the panel
*
* @deprecated 3.6.0 Use `->panel()->url()` instead
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @param bool $relative
* @return string
* @codeCoverageIgnore
*/
public function panelUrl(bool $relative = false): string
{
return $this->panel()->url($relative);
}
}

View file

@ -7,8 +7,10 @@ use Kirby\Exception\DuplicateException;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Exception\NotFoundException;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Form\Form;
use Kirby\Toolkit\A;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Str;
/**
@ -284,6 +286,10 @@ trait PageActions
]);
foreach ($this->kirby()->languages()->codes() as $code) {
if ($oldPage->translation($code)->exists() !== true) {
continue;
}
$content = $oldPage->content($code)->convertTo($template);
if (F::remove($oldPage->contentFile($code)) !== true) {
@ -295,7 +301,7 @@ trait PageActions
}
// return a fresh copy of the object
return $newPage->clone();
$page = $newPage->clone();
} else {
$newPage = $this->clone([
'content' => $this->content()->convertTo($template),
@ -306,8 +312,17 @@ trait PageActions
throw new LogicException('The old text file could not be removed');
}
return $newPage->save();
$page = $newPage->save();
}
// update the parent collection
if ($page->isDraft() === true) {
$page->parentModel()->drafts()->set($page->id(), $page);
} else {
$page->parentModel()->children()->set($page->id(), $page);
}
return $page;
});
}
@ -322,7 +337,16 @@ trait PageActions
{
$arguments = ['page' => $this, 'title' => $title, 'languageCode' => $languageCode];
return $this->commit('changeTitle', $arguments, function ($page, $title, $languageCode) {
return $page->save(['title' => $title], $languageCode);
$page = $page->save(['title' => $title], $languageCode);
// flush the parent cache to get children and drafts right
if ($page->isDraft() === true) {
$page->parentModel()->drafts()->set($page->id(), $page);
} else {
$page->parentModel()->children()->set($page->id(), $page);
}
return $page;
});
}
@ -529,14 +553,13 @@ trait PageActions
return 0;
case 'date':
case 'datetime':
// the $format needs to produce only digits,
// so it can be converted to integer below
$format = $mode === 'date' ? 'Ymd' : 'YmdHi';
$lang = $this->kirby()->defaultLanguage() ?? null;
$field = $this->content($lang)->get('date');
$date = $field->isEmpty() ? 'now' : $field;
// TODO: in 3.6.0 throw an error if date() doesn't
// return a number, see https://github.com/getkirby/kirby/pull/3061#discussion_r552783943
return (int)date($format, strtotime($date));
break;
case 'default':
$max = $this
@ -571,7 +594,7 @@ trait PageActions
'kirby' => $app,
'page' => $app->page($this->id()),
'site' => $app->site(),
], '');
], ['fallback' => '']);
return (int)$template;
}
@ -640,17 +663,28 @@ trait PageActions
{
// create the slug for the duplicate
$slug = Str::slug($slug ?? $this->slug() . '-copy');
$slug = Str::slug($slug ?? $this->slug() . '-' . Str::slug(t('page.duplicate.appendix')));
$arguments = [
'originalPage' => $this,
'input' => $slug,
'options' => $options
];
$arguments = ['originalPage' => $this, 'input' => $slug, 'options' => $options];
return $this->commit('duplicate', $arguments, function ($page, $slug, $options) {
return $this->copy([
$page = $this->copy([
'parent' => $this->parent(),
'slug' => $slug,
'isDraft' => true,
'files' => $options['files'] ?? false,
'children' => $options['children'] ?? false,
]);
if (isset($options['title']) === true) {
$page = $page->changeTitle($options['title']);
}
return $page;
});
}
@ -786,20 +820,6 @@ trait PageActions
return true;
}
/**
* @deprecated 3.5.0 Use `Page::changeSort()` instead
* @todo Remove in 3.6.0
*
* @param null $position
* @return $this|static
*/
public function sort($position = null)
{
deprecated('$page->sort() is deprecated, use $page->changeSort() instead. $page->sort() will be removed in Kirby 3.6.0.');
return $this->changeStatus('listed', $position);
}
/**
* Convert a page from listed or
* unlisted to draft.

View file

@ -169,26 +169,12 @@ class PageRules
return true;
}
if ($page->permissions()->changeStatus() !== true) {
throw new PermissionException([
'key' => 'page.changeStatus.permission',
'data' => [
'slug' => $page->slug()
]
]);
}
static::publish($page);
if ($position !== null && $position < 0) {
throw new InvalidArgumentException(['key' => 'page.num.invalid']);
}
if ($page->isDraft() === true && empty($page->errors()) === false) {
throw new PermissionException([
'key' => 'page.changeStatus.incomplete',
'details' => $page->errors()
]);
}
return true;
}
@ -201,14 +187,7 @@ class PageRules
*/
public static function changeStatusToUnlisted(Page $page)
{
if ($page->permissions()->changeStatus() !== true) {
throw new PermissionException([
'key' => 'page.changeStatus.permission',
'data' => [
'slug' => $page->slug()
]
]);
}
static::publish($page);
return true;
}
@ -376,6 +355,34 @@ class PageRules
return true;
}
/**
* Check if the page can be published
* (status change from draft to listed or unlisted)
*
* @param Page $page
* @return bool
*/
public static function publish(Page $page): bool
{
if ($page->permissions()->changeStatus() !== true) {
throw new PermissionException([
'key' => 'page.changeStatus.permission',
'data' => [
'slug' => $page->slug()
]
]);
}
if ($page->isDraft() === true && empty($page->errors()) === false) {
throw new PermissionException([
'key' => 'page.changeStatus.incomplete',
'details' => $page->errors()
]);
}
return true;
}
/**
* Validates if the page can be updated
*

View file

@ -48,13 +48,13 @@ class Pages extends Collection
* an entire second collection to the
* current collection
*
* @param mixed $object
* @param \Kirby\Cms\Pages|\Kirby\Cms\Page|string $object
* @return $this
* @throws \Kirby\Exception\InvalidArgumentException
* @throws \Kirby\Exception\InvalidArgumentException When no `Page` or `Pages` object or an ID of an existing page is passed
*/
public function add($object)
{
// add a page collection
// add a pages collection
if (is_a($object, self::class) === true) {
$this->data = array_merge($this->data, $object->data);
@ -66,9 +66,10 @@ class Pages extends Collection
} elseif (is_a($object, 'Kirby\Cms\Page') === true) {
$this->__set($object->id(), $object);
// give a useful error message on invalid input
// give a useful error message on invalid input;
// silently ignore "empty" values for compatibility with existing setups
} elseif (in_array($object, [null, false, true], true) !== true) {
throw new InvalidArgumentException('You must pass a Page object to the Pages collection');
throw new InvalidArgumentException('You must pass a Pages or Page object or an ID of an existing page to the Pages collection');
}
return $this;
@ -223,14 +224,8 @@ class Pages extends Collection
return $page;
}
$multiLang = App::instance()->multilang();
if ($multiLang === true && $page = $this->findBy('slug', $id)) {
return $page;
}
$start = is_a($this->parent, 'Kirby\Cms\Page') === true ? $this->parent->id() : '';
$page = $this->findByIdRecursive($id, $start, $multiLang);
$page = $this->findByIdRecursive($id, $start, App::instance()->multilang());
return $page;
}
@ -254,8 +249,14 @@ class Pages extends Collection
$query = ltrim($query . '/' . $key, '/');
$item = $collection->get($query) ?? null;
if ($item === null && $multiLang === true) {
$item = $collection->findBy('slug', $key);
if ($item === null && $multiLang === true && !App::instance()->language()->isDefault()) {
if (count($path) > 1 || $collection->parent()) {
// either the desired path is definitely not a slug, or collection is the children of another collection
$item = $collection->findBy('slug', $key);
} else {
// desired path _could_ be a slug or a "top level" uri
$item = $collection->findBy('uri', $key);
}
}
if ($item === null) {

View file

@ -1,139 +0,0 @@
<?php
namespace Kirby\Cms;
use Exception;
use Kirby\Http\Response;
use Kirby\Http\Uri;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\View;
use Throwable;
/**
* The Panel class is only responsible to create
* a working panel view with all the right URLs
* and other panel options. The view template is
* located in `kirby/views/panel.php`
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Panel
{
/**
* Returns custom css path for panel ui
*
* @param \Kirby\Cms\App $kirby
* @return string|false
*/
public static function customCss(App $kirby)
{
if ($css = $kirby->option('panel.css')) {
$asset = asset($css);
if ($asset->exists() === true) {
return $asset->url() . '?' . $asset->modified();
}
}
return false;
}
/**
* Returns predefined icons path as sprite svg file
*
* @param \Kirby\Cms\App $kirby
* @return string
*/
public static function icons(App $kirby): string
{
return F::read($kirby->root('kirby') . '/panel/dist/img/icons.svg');
}
/**
* Links all dist files in the media folder
* and returns the link to the requested asset
*
* @param \Kirby\Cms\App $kirby
* @return bool
* @throws \Exception If Panel assets could not be moved to the public directory
*/
public static function link(App $kirby): bool
{
$mediaRoot = $kirby->root('media') . '/panel';
$panelRoot = $kirby->root('panel') . '/dist';
$versionHash = $kirby->versionHash();
$versionRoot = $mediaRoot . '/' . $versionHash;
// check if the version already exists
if (is_dir($versionRoot) === true) {
return false;
}
// delete the panel folder and all previous versions
Dir::remove($mediaRoot);
// recreate the panel folder
Dir::make($mediaRoot, true);
// create a symlink to the dist folder
if (Dir::copy($panelRoot, $versionRoot) !== true) {
throw new Exception('Panel assets could not be linked');
}
return true;
}
/**
* Renders the main panel view
*
* @param \Kirby\Cms\App $kirby
* @return \Kirby\Http\Response
*/
public static function render(App $kirby)
{
try {
if (static::link($kirby) === true) {
usleep(1);
go($kirby->url('index') . '/' . $kirby->path());
}
} catch (Throwable $e) {
die('The Panel assets cannot be installed properly. ' . $e->getMessage());
}
// get the uri object for the panel url
$uri = new Uri($url = $kirby->url('panel'));
// fetch all plugins
$plugins = new PanelPlugins();
$view = new View($kirby->root('kirby') . '/views/panel.php', [
'kirby' => $kirby,
'config' => $kirby->option('panel'),
'assetUrl' => $kirby->url('media') . '/panel/' . $kirby->versionHash(),
'customCss' => static::customCss($kirby),
'icons' => static::icons($kirby),
'pluginCss' => $plugins->url('css'),
'pluginJs' => $plugins->url('js'),
'panelUrl' => $uri->path()->toString(true) . '/',
'nonce' => $kirby->nonce(),
'options' => [
'url' => $url,
'site' => $kirby->url('index'),
'api' => $kirby->url('api'),
'csrf' => $kirby->option('api.csrf') ?? csrf(),
'translation' => 'en',
'debug' => $kirby->option('debug', false),
'search' => [
'limit' => $kirby->option('panel.search.limit') ?? 10
]
]
]);
return new Response($view->render());
}
}

View file

@ -1,108 +0,0 @@
<?php
namespace Kirby\Cms;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Str;
/**
* The PanelPlugins class takes care of collecting
* js and css plugin files for the panel and caches
* them in the media folder
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class PanelPlugins
{
/**
* Cache of all collected plugin files
*
* @var array
*/
public $files;
/**
* Collects and returns the plugin files for all plugins
*
* @return array
*/
public function files(): array
{
if ($this->files !== null) {
return $this->files;
}
$this->files = [];
foreach (App::instance()->plugins() as $plugin) {
$this->files[] = $plugin->root() . '/index.css';
$this->files[] = $plugin->root() . '/index.js';
}
return $this->files;
}
/**
* Returns the last modification
* of the collected plugin files
*
* @return int
*/
public function modified(): int
{
$files = $this->files();
$modified = [0];
foreach ($files as $file) {
$modified[] = F::modified($file);
}
return max($modified);
}
/**
* Read the files from all plugins and concatenate them
*
* @param string $type
* @return string
*/
public function read(string $type): string
{
$dist = [];
foreach ($this->files() as $file) {
if (F::extension($file) === $type) {
if ($content = F::read($file)) {
if ($type === 'js') {
$content = trim($content);
// make sure that each plugin is ended correctly
if (Str::endsWith($content, ';') === false) {
$content .= ';';
}
}
$dist[] = $content;
}
}
}
return implode(PHP_EOL . PHP_EOL, $dist);
}
/**
* Absolute url to the cache file
* This is used by the panel to link the plugins
*
* @param string $type
* @return string
*/
public function url(string $type): string
{
return App::instance()->url('media') . '/plugins/index.' . $type . '?' . $this->modified();
}
}

View file

@ -27,10 +27,12 @@ class Permissions
*/
protected $actions = [
'access' => [
'panel' => true,
'settings' => true,
'site' => true,
'users' => true,
'account' => true,
'languages' => true,
'panel' => true,
'site' => true,
'system' => true,
'users' => true,
],
'files' => [
'changeName' => true,
@ -157,6 +159,12 @@ class Permissions
*/
protected function setAction(string $category, string $action, $setting)
{
// deprecated fallback for the settings/system view
// TODO: remove in 3.7
if ($category === 'access' && $action === 'settings') {
$action = 'system';
}
// wildcard to overwrite the entire category
if ($action === '*') {
return $this->setCategory($category, $setting);

View file

@ -54,6 +54,8 @@ abstract class Picker
'image' => [],
// query template for the info field
'info' => false,
// listing style: list, cards, cardlets
'layout' =>'list',
// number of users displayed per pagination page
'limit' => 20,
// optional mapping function for the result array
@ -98,11 +100,12 @@ abstract class Picker
if (empty($this->options['map']) === false) {
$result[] = $this->options['map']($item);
} else {
$result[] = $item->panelPickerData([
'image' => $this->options['image'],
'info' => $this->options['info'],
'model' => $this->options['model'],
'text' => $this->options['text'],
$result[] = $item->panel()->pickerData([
'image' => $this->options['image'],
'info' => $this->options['info'],
'layout' => $this->options['layout'],
'model' => $this->options['model'],
'text' => $this->options['text'],
]);
}
}

View file

@ -5,6 +5,7 @@ namespace Kirby\Cms;
use Exception;
use Kirby\Data\Data;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\V;
/**
* Represents a Plugin and handles parsing of
@ -45,8 +46,36 @@ class Plugin extends Model
$this->setName($name);
$this->extends = $extends;
$this->root = $extends['root'] ?? dirname(debug_backtrace()[0]['file']);
$this->info = empty($extends['info']) === false && is_array($extends['info']) ? $extends['info'] : null;
unset($this->extends['root']);
unset($this->extends['root'], $this->extends['info']);
}
/**
* Returns the array with author information
* from the composer file
*
* @return array
*/
public function authors(): array
{
return $this->info()['authors'] ?? [];
}
/**
* Returns a comma-separated list with all author names
*
* @return string
*/
public function authorsNames(): string
{
$names = [];
foreach ($this->authors() as $author) {
$names[] = $author['name'] ?? null;
}
return implode(', ', array_filter($names));
}
/**
@ -57,6 +86,16 @@ class Plugin extends Model
return $this->extends;
}
/**
* Returns the unique id for the plugin
*
* @return string
*/
public function id(): string
{
return $this->name();
}
/**
* @return array
*/
@ -76,6 +115,22 @@ class Plugin extends Model
return $this->info = $info;
}
/**
* Returns the link to the plugin homepage
*
* @return string|null
*/
public function link(): ?string
{
$homepage = $this->info['homepage'] ?? null;
$docs = $this->info['support']['docs'] ?? null;
$source = $this->info['support']['source'] ?? null;
$link = $homepage ?? $docs ?? $source;
return V::url($link) ? $link : null;
}
/**
* @return string
*/
@ -153,6 +208,14 @@ class Plugin extends Model
*/
public function toArray(): array
{
return $this->propertiesToArray();
return [
'authors' => $this->authors(),
'description' => $this->description(),
'name' => $this->name(),
'license' => $this->license(),
'link' => $this->link(),
'root' => $this->root(),
'version' => $this->version()
];
}
}

View file

@ -2,9 +2,9 @@
namespace Kirby\Cms;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Http\Response;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
/**
* Plugin assets are automatically copied/linked
@ -66,15 +66,11 @@ class PluginAssets
static::clean($pluginName);
$target = $plugin->mediaRoot() . '/' . $filename;
$url = $plugin->mediaUrl() . '/' . $filename;
// create the plugin directory first
Dir::make($plugin->mediaRoot(), true);
if (F::link($source, $target, 'symlink') === true) {
return Response::redirect($url);
}
// create a symlink if possible
F::link($source, $target, 'symlink');
// return the file response
return Response::file($source);
}
}

View file

@ -3,7 +3,7 @@
namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\Mime;
use Kirby\Filesystem\Mime;
use Kirby\Toolkit\Str;
/**

View file

@ -4,7 +4,7 @@ namespace Kirby\Cms;
use Exception;
use Kirby\Data\Data;
use Kirby\Toolkit\F;
use Kirby\Filesystem\F;
use Kirby\Toolkit\I18n;
/**

View file

@ -4,6 +4,8 @@ namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Filesystem\Dir;
use Kirby\Panel\Site as Panel;
use Kirby\Toolkit\A;
/**
@ -113,7 +115,7 @@ class Site extends ModelWithContent
}
// return site content otherwise
return $this->content()->get($method, $arguments);
return $this->content()->get($method);
}
/**
@ -412,31 +414,13 @@ class Site extends ModelWithContent
}
/**
* Returns the full path without leading slash
* Returns the panel info object
*
* @internal
* @return string
* @return \Kirby\Panel\Site
*/
public function panelPath(): string
public function panel()
{
return 'site';
}
/**
* Returns the url to the editing view
* in the panel
*
* @internal
* @param bool $relative
* @return string
*/
public function panelUrl(bool $relative = false): string
{
if ($relative === true) {
return '/' . $this->panelPath();
} else {
return $this->kirby()->url('panel') . '/' . $this->panelPath();
}
return new Panel($this);
}
/**
@ -571,7 +555,7 @@ class Site extends ModelWithContent
* @param string|null $url
* @return $this
*/
protected function setUrl($url = null)
protected function setUrl(?string $url = null)
{
$this->url = $url;
return $this;
@ -603,7 +587,7 @@ class Site extends ModelWithContent
* @param string|null $language
* @return string
*/
public function url($language = null): string
public function url(?string $language = null): string
{
if ($language !== null || $this->kirby()->multilang() === true) {
return $this->urlForLanguage($language);
@ -675,4 +659,41 @@ class Site extends ModelWithContent
{
return Dir::wasModifiedAfter($this->root(), $time);
}
/**
* Deprecated!
*/
/**
* Returns the full path without leading slash
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @return string
* @codeCoverageIgnore
*/
public function panelPath(): string
{
return $this->panel()->path();
}
/**
* Returns the url to the editing view
* in the panel
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @param bool $relative
* @return string
* @codeCoverageIgnore
*/
public function panelUrl(bool $relative = false): string
{
return $this->panel()->url($relative);
}
}

View file

@ -55,7 +55,10 @@ trait SiteActions
*/
public function changeTitle(string $title, string $languageCode = null)
{
$arguments = ['site' => $this, 'title' => $title, 'languageCode' => $languageCode];
$site = $this;
$title = trim($title);
$arguments = compact('site', 'title', 'languageCode');
return $this->commit('changeTitle', $arguments, function ($site, $title, $languageCode) {
return $site->save(['title' => $title], $languageCode);
});

View file

@ -3,11 +3,11 @@
namespace Kirby\Cms;
/**
* The StructureObject reprents each item
* The StructureObject represents each item
* in a Structure collection. StructureObjects
* behave pretty much the same as Pages or Users
* and have a Content object to access their fields.
* All fields in a StructureObject are therefor also
* All fields in a StructureObject are therefore also
* wrapped in a Field object and can be accessed in
* the same way as Page fields. They also use the same
* Field methods.
@ -61,7 +61,7 @@ class StructureObject extends Model
return $this->$method;
}
return $this->content()->get($method, $arguments);
return $this->content()->get($method);
}
/**

View file

@ -6,10 +6,10 @@ use Kirby\Data\Json;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\PermissionException;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Http\Remote;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\V;
use Throwable;
@ -56,25 +56,6 @@ class System
return $this->toArray();
}
/**
* Get an status array of all checks
*
* @return array
*/
public function status(): array
{
return [
'accounts' => $this->accounts(),
'content' => $this->content(),
'curl' => $this->curl(),
'sessions' => $this->sessions(),
'mbstring' => $this->mbstring(),
'media' => $this->media(),
'php' => $this->php(),
'server' => $this->server(),
];
}
/**
* Check for a writable accounts folder
*
@ -132,6 +113,13 @@ class System
throw new PermissionException('The accounts directory could not be created');
}
// init /site/sessions
try {
Dir::make($this->app->root('sessions'));
} catch (Throwable $e) {
throw new PermissionException('The sessions directory could not be created');
}
// init /content
try {
Dir::make($this->app->root('content'));
@ -227,44 +215,6 @@ class System
return in_array(false, array_values($this->status()), true) === false;
}
/**
* Normalizes the app's index URL for
* licensing purposes
*
* @param string|null $url Input URL, by default the app's index URL
* @return string Normalized URL
*/
protected function licenseUrl(string $url = null): string
{
if ($url === null) {
$url = $this->indexUrl();
}
// remove common "testing" subdomains as well as www.
// to ensure that installations of the same site have
// the same license URL; only for installations at /,
// subdirectory installations are difficult to normalize
if (Str::contains($url, '/') === false) {
if (Str::startsWith($url, 'www.')) {
return substr($url, 4);
}
if (Str::startsWith($url, 'dev.')) {
return substr($url, 4);
}
if (Str::startsWith($url, 'test.')) {
return substr($url, 5);
}
if (Str::startsWith($url, 'staging.')) {
return substr($url, 8);
}
}
return $url;
}
/**
* Loads the license file and returns
* the license information if available
@ -276,7 +226,7 @@ class System
public function license()
{
try {
$license = Json::read($this->app->root('config') . '/.license');
$license = Json::read($this->app->root('license'));
} catch (Throwable $e) {
return false;
}
@ -319,13 +269,51 @@ class System
// only return the actual license key if the
// current user has appropriate permissions
$user = $this->app->user();
if ($user && $user->role()->permissions()->for('access', 'settings') === true) {
if ($user && $user->isAdmin() === true) {
return $license['license'];
} else {
return true;
}
}
/**
* Normalizes the app's index URL for
* licensing purposes
*
* @param string|null $url Input URL, by default the app's index URL
* @return string Normalized URL
*/
protected function licenseUrl(string $url = null): string
{
if ($url === null) {
$url = $this->indexUrl();
}
// remove common "testing" subdomains as well as www.
// to ensure that installations of the same site have
// the same license URL; only for installations at /,
// subdirectory installations are difficult to normalize
if (Str::contains($url, '/') === false) {
if (Str::startsWith($url, 'www.')) {
return substr($url, 4);
}
if (Str::startsWith($url, 'dev.')) {
return substr($url, 4);
}
if (Str::startsWith($url, 'test.')) {
return substr($url, 5);
}
if (Str::startsWith($url, 'staging.')) {
return substr($url, 8);
}
}
return $url;
}
/**
* Returns the configured UI modes for the login form
* with their respective options
@ -416,10 +404,21 @@ class System
public function php(): bool
{
return
version_compare(PHP_VERSION, '7.3.0', '>=') === true &&
version_compare(PHP_VERSION, '7.4.0', '>=') === true &&
version_compare(PHP_VERSION, '8.1.0', '<') === true;
}
/**
* Returns a sorted collection of all
* installed plugins
*
* @return \Kirby\Cms\Collection
*/
public function plugins()
{
return (new Collection(App::instance()->plugins()))->sortBy('name', 'asc');
}
/**
* Validates the license key
* and adds it to the .license file in the config
@ -445,10 +444,11 @@ class System
]);
}
// @codeCoverageIgnoreStart
$response = Remote::get('https://licenses.getkirby.com/register', [
'data' => [
'license' => $license,
'email' => $email,
'email' => Str::lower(trim($email)),
'domain' => $this->indexUrl()
]
]);
@ -464,7 +464,7 @@ class System
$json['email'] = $email;
// where to store the license file
$file = $this->app->root('config') . '/.license';
$file = $this->app->root('license');
// save the license information
Json::write($file, $json);
@ -474,6 +474,7 @@ class System
'key' => 'license.verification'
]);
}
// @codeCoverageIgnoreEnd
return true;
}
@ -484,6 +485,16 @@ class System
* @return bool
*/
public function server(): bool
{
return $this->serverSoftware() !== null;
}
/**
* Returns the detected server software
*
* @return string|null
*/
public function serverSoftware(): ?string
{
if ($servers = $this->app->option('servers')) {
$servers = A::wrap($servers);
@ -499,7 +510,9 @@ class System
$software = $_SERVER['SERVER_SOFTWARE'] ?? null;
return preg_match('!(' . implode('|', $servers) . ')!i', $software) > 0;
preg_match('!(' . implode('|', $servers) . ')!i', $software, $matches);
return $matches[0] ?? null;
}
/**
@ -513,10 +526,45 @@ class System
}
/**
* Return the status as array
* Get an status array of all checks
*
* @return array
*/
public function status(): array
{
return [
'accounts' => $this->accounts(),
'content' => $this->content(),
'curl' => $this->curl(),
'sessions' => $this->sessions(),
'mbstring' => $this->mbstring(),
'media' => $this->media(),
'php' => $this->php(),
'server' => $this->server(),
];
}
/**
* Returns the site's title as defined in the
* content file or `site.yml` blueprint
* @since 3.6.0
*
* @return string
*/
public function title(): string
{
$site = $this->app->site();
if ($site->title()->isNotEmpty()) {
return $site->title()->value();
}
return $site->blueprint()->title();
}
/**
* @return array
*/
public function toArray(): array
{
return $this->status();

View file

@ -3,7 +3,7 @@
namespace Kirby\Cms;
use Exception;
use Kirby\Toolkit\F;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Tpl;
/**

View file

@ -2,8 +2,8 @@
namespace Kirby\Cms;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
/**
* A collection of all available Translations.

View file

@ -61,9 +61,6 @@ class Url extends BaseUrl
public static function to(string $path = null, $options = null): string
{
$kirby = App::instance();
return ($kirby->component('url'))($kirby, $path, $options, function (string $path = null, $options = null) use ($kirby) {
return ($kirby->nativeComponent('url'))($kirby, $path, $options);
});
return ($kirby->component('url'))($kirby, $path, $options);
}
}

View file

@ -5,8 +5,9 @@ namespace Kirby\Cms;
use Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\F;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Panel\User as Panel;
use Kirby\Toolkit\Str;
/**
@ -115,7 +116,7 @@ class User extends ModelWithContent
}
// return site content otherwise
return $this->content()->get($method, $arguments);
return $this->content()->get($method);
}
/**
@ -125,7 +126,8 @@ class User extends ModelWithContent
*/
public function __construct(array $props)
{
$props['id'] = $props['id'] ?? $this->createId();
// TODO: refactor later to avoid redundant prop setting
$this->setProperty('id', $props['id'] ?? $this->createId(), true);
$this->setProperties($props);
}
@ -578,92 +580,13 @@ class User extends ModelWithContent
}
/**
* Panel icon definition
* Returns the panel info object
*
* @internal
* @param array $params
* @return array
* @return \Kirby\Panel\User
*/
public function panelIcon(array $params = null): array
public function panel()
{
$params['type'] = 'user';
return parent::panelIcon($params);
}
/**
* Returns the image file object based on provided query
*
* @internal
* @param string|null $query
* @return \Kirby\Cms\File|\Kirby\Cms\Asset|null
*/
protected function panelImageSource(string $query = null)
{
if ($query === null) {
return $this->avatar();
}
return parent::panelImageSource($query);
}
/**
* Returns the full path without leading slash
*
* @internal
* @return string
*/
public function panelPath(): string
{
return 'users/' . $this->id();
}
/**
* Returns prepared data for the panel user picker
*
* @param array|null $params
* @return array
*/
public function panelPickerData(array $params = null): array
{
$image = $this->panelImage($params['image'] ?? []);
$icon = $this->panelIcon($image);
// escape the default text
// TODO: no longer needed in 3.6
$textQuery = $params['text'] ?? '{{ user.username }}';
$text = $this->toString($textQuery);
if ($textQuery === '{{ user.username }}') {
$text = Escape::html($text);
}
return [
'icon' => $icon,
'id' => $this->id(),
'image' => $image,
'email' => $this->email(),
'info' => $this->toString($params['info'] ?? false),
'link' => $this->panelUrl(true),
'text' => $text,
'username' => $this->username(),
];
}
/**
* Returns the url to the editing view
* in the panel
*
* @internal
* @param bool $relative
* @return string
*/
public function panelUrl(bool $relative = false): string
{
if ($relative === true) {
return '/' . $this->panelPath();
} else {
return $this->kirby()->url('panel') . '/' . $this->panelPath();
}
return new Panel($this);
}
/**
@ -909,13 +832,13 @@ class User extends ModelWithContent
* @param string $fallback Fallback for tokens in the template that cannot be replaced
* @return string
*/
public function toString(string $template = null, array $data = [], string $fallback = ''): string
public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string
{
if ($template === null) {
$template = $this->email();
}
return parent::toString($template, $data);
return parent::toString($template, $data, $fallback, $handler);
}
/**
@ -951,9 +874,61 @@ class User extends ModelWithContent
}
if (password_verify($password, $this->password()) !== true) {
throw new InvalidArgumentException(['key' => 'user.password.wrong']);
throw new InvalidArgumentException(['key' => 'user.password.wrong', 'httpCode' => 401]);
}
return true;
}
/**
* Deprecated!
*/
/**
* Returns the full path without leading slash
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @return string
* @codeCoverageIgnore
*/
public function panelPath(): string
{
return $this->panel()->path();
}
/**
* Returns prepared data for the panel user picker
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @param array|null $params
* @return array
* @codeCoverageIgnore
*/
public function panelPickerData(array $params = null): array
{
return $this->panel()->pickerData($params);
}
/**
* Returns the url to the editing view
* in the panel
*
* @todo Add `deprecated()` helper warning in 3.7.0
* @todo Remove in 3.8.0
*
* @internal
* @param bool $relative
* @return string
* @codeCoverageIgnore
*/
public function panelUrl(bool $relative = false): string
{
return $this->panel()->url($relative);
}
}

View file

@ -6,10 +6,12 @@ use Closure;
use Kirby\Data\Data;
use Kirby\Exception\LogicException;
use Kirby\Exception\PermissionException;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Form\Form;
use Kirby\Http\Idn;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Str;
use Throwable;
/**
* UserActions
@ -30,6 +32,8 @@ trait UserActions
*/
public function changeEmail(string $email)
{
$email = trim($email);
return $this->commit('changeEmail', ['user' => $this, 'email' => Idn::decodeEmail($email)], function ($user, $email) {
$user = $user->clone([
'email' => $email
@ -39,6 +43,9 @@ trait UserActions
'email' => $email
]);
// update the users collection
$user->kirby()->users()->set($user->id(), $user);
return $user;
});
}
@ -60,6 +67,9 @@ trait UserActions
'language' => $language
]);
// update the users collection
$user->kirby()->users()->set($user->id(), $user);
return $user;
});
}
@ -72,6 +82,8 @@ trait UserActions
*/
public function changeName(string $name)
{
$name = trim($name);
return $this->commit('changeName', ['user' => $this, 'name' => $name], function ($user, $name) {
$user = $user->clone([
'name' => $name
@ -81,6 +93,9 @@ trait UserActions
'name' => $name
]);
// update the users collection
$user->kirby()->users()->set($user->id(), $user);
return $user;
});
}
@ -100,6 +115,9 @@ trait UserActions
$user->writePassword($password);
// update the users collection
$user->kirby()->users()->set($user->id(), $user);
return $user;
});
}
@ -121,6 +139,9 @@ trait UserActions
'role' => $role
]);
// update the users collection
$user->kirby()->users()->set($user->id(), $user);
return $user;
});
}
@ -232,14 +253,21 @@ trait UserActions
public function createId(): string
{
$length = 8;
$id = Str::random($length);
while ($this->kirby()->users()->has($id)) {
$length++;
$id = Str::random($length);
}
do {
try {
$id = Str::random($length);
if (UserRules::validId($this, $id) === true) {
return $id;
}
return $id;
// we can't really test for a random match
// @codeCoverageIgnoreStart
} catch (Throwable $e) {
$length++;
}
} while (true);
// @codeCoverageIgnoreEnd
}
/**
@ -315,6 +343,9 @@ trait UserActions
$this->kirby()->auth()->setUser($user);
}
// update the users collection
$user->kirby()->users()->set($user->id(), $user);
return $user;
}
@ -327,6 +358,11 @@ trait UserActions
*/
protected function updateCredentials(array $credentials): bool
{
// normalize the email address
if (isset($credentials['email']) === true) {
$credentials['email'] = Str::lower(trim($credentials['email']));
}
return $this->writeCredentials(array_merge($this->credentials(), $credentials));
}

View file

@ -299,6 +299,10 @@ class UserRules
*/
public static function validId(User $user, string $id): bool
{
if ($id === 'account') {
throw new InvalidArgumentException('"account" is a reserved word and cannot be used as user id');
}
if ($user->kirby()->users()->find($id)) {
throw new DuplicateException('A user with this id exists');
}

View file

@ -2,8 +2,9 @@
namespace Kirby\Cms;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Toolkit\Str;
/**
@ -37,12 +38,13 @@ class Users extends Collection
* an entire second collection to the
* current collection
*
* @param mixed $object
* @param \Kirby\Cms\Users|\Kirby\Cms\User|string $object
* @return $this
* @throws \Kirby\Exception\InvalidArgumentException When no `User` or `Users` object or an ID of an existing user is passed
*/
public function add($object)
{
// add a page collection
// add a users collection
if (is_a($object, self::class) === true) {
$this->data = array_merge($this->data, $object->data);
@ -53,6 +55,11 @@ class Users extends Collection
// add a user object
} elseif (is_a($object, 'Kirby\Cms\User') === true) {
$this->__set($object->id(), $object);
// give a useful error message on invalid input;
// silently ignore "empty" values for compatibility with existing setups
} elseif (in_array($object, [null, false, true], true) !== true) {
throw new InvalidArgumentException('You must pass a Users or User object or an ID of an existing user to the Users collection');
}
return $this;