This commit is contained in:
Bastian Allgeier
2020-07-07 12:40:13 +02:00
parent 5f025ac2c2
commit f79d2e960c
176 changed files with 10532 additions and 5343 deletions

View File

@@ -39,7 +39,8 @@ class Api extends BaseApi
$this->kirby->setCurrentLanguage($this->language());
if ($user = $this->kirby->user()) {
$allowImpersonation = $this->kirby()->option('api.allowImpersonation', false);
if ($user = $this->kirby->user(null, $allowImpersonation)) {
$this->kirby->setCurrentTranslation($user->language());
}
@@ -95,8 +96,9 @@ class Api extends BaseApi
public function file(string $path = null, string $filename)
{
$filename = urldecode($filename);
$file = $this->parent($path)->file($filename);
if ($file = $this->parent($path)->file($filename)) {
if ($file && $file->isReadable() === true) {
return $file;
}
@@ -136,7 +138,7 @@ class Api extends BaseApi
$model = $kirby->site();
break;
case 'account':
$model = $kirby->user();
$model = $kirby->user(null, $kirby->option('api.allowImpersonation', false));
break;
case 'page':
$id = str_replace(['+', ' '], '/', basename($path));
@@ -192,7 +194,7 @@ class Api extends BaseApi
$id = str_replace('+', '/', $id);
$page = $this->kirby->page($id);
if ($page && $page->isReadable()) {
if ($page && $page->isReadable() === true) {
return $page;
}
@@ -242,7 +244,7 @@ class Api extends BaseApi
{
// get the authenticated user
if ($id === null) {
return $this->kirby->auth()->user();
return $this->kirby->auth()->user(null, $this->kirby()->option('api.allowImpersonation', false));
}
// get a specific user by id

View File

@@ -6,6 +6,7 @@ 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\Http\Request;
use Kirby\Http\Router;
@@ -18,6 +19,7 @@ use Kirby\Toolkit\Controller;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Properties;
use Throwable;
/**
* The `$kirby` object is the app instance of
@@ -44,7 +46,6 @@ class App
use Properties;
protected static $instance;
protected static $root;
protected static $version;
public $data = [];
@@ -74,6 +75,14 @@ class App
protected $users;
protected $visitor;
/**
* List of options that shouldn't be converted
* to a tree structure by dot syntax
*
* @var array
*/
public static $nestIgnoreOptions = ['hooks'];
/**
* Creates a new App instance
*
@@ -81,9 +90,6 @@ class App
*/
public function __construct(array $props = [])
{
// the kirby folder directory
static::$root = dirname(__DIR__, 2);
// register all roots to be able to load stuff afterwards
$this->bakeRoots($props['roots'] ?? []);
@@ -91,6 +97,9 @@ class App
$this->optionsFromConfig();
$this->optionsFromProps($props['options'] ?? []);
// register the Whoops error handler
$this->handleErrors();
// set the path to make it available for the url bakery
$this->setPath($props['path'] ?? null);
@@ -118,20 +127,22 @@ class App
$this->extensionsFromSystem();
$this->extensionsFromProps($props);
$this->extensionsFromPlugins();
$this->extensionsFromOptions();
$this->extensionsFromFolders();
// bake the options for the first time
$this->bakeOptions();
// register the extensions from the normalized options
$this->extensionsFromOptions();
// trigger hook for use in plugins
$this->trigger('system.loadPlugins:after');
// handle those damn errors
$this->handleErrors();
// execute a ready callback from the config
$this->optionsFromReadyCallback();
// bake config
Config::$data = $this->options;
// bake the options again with those from the ready callback
$this->bakeOptions();
}
/**
@@ -164,7 +175,7 @@ class App
return $this->api;
}
$root = static::$root . '/config/api';
$root = $this->root('kirby') . '/config/api';
$extensions = $this->extensions['api'] ?? [];
$routes = (include $root . '/routes.php')($this);
@@ -182,37 +193,54 @@ class App
}
/**
* Applies a hook to the given value;
* the value that gets modified by the hooks
* is always the last argument
* Applies a hook to the given value
*
* @internal
* @param string $name Hook name
* @param mixed ...$args Arguments to pass to the hooks
* @param string $name Full event name
* @param array $args Associative array of named event arguments
* @param string $modify Key in $args that is modified by the hooks
* @param \Kirby\Cms\Event|null $originalEvent Event object (internal use)
* @return mixed Resulting value as modified by the hooks
*/
public function apply(string $name, ...$args)
public function apply(string $name, array $args, string $modify, ?Event $originalEvent = null)
{
// split up args into "passive" args and the value
$value = array_pop($args);
$event = $originalEvent ?? new Event($name, $args);
if ($functions = $this->extension('hooks', $name)) {
foreach ($functions as $function) {
// re-assemble args
$hookArgs = $args;
$hookArgs[] = $value;
// bind the App object to the hook
$newValue = $function->call($this, ...$hookArgs);
$newValue = $event->call($this, $function);
// update value if one was returned
if ($newValue !== null) {
$value = $newValue;
$event->updateArgument($modify, $newValue);
}
}
}
return $value;
// apply wildcard hooks if available
$nameWildcards = $event->nameWildcards();
if ($originalEvent === null && count($nameWildcards) > 0) {
foreach ($nameWildcards as $nameWildcard) {
// the $event object is passed by reference
// and will be modified down the chain
$this->apply($nameWildcard, $event->arguments(), $modify, $event);
}
}
return $event->argument($modify);
}
/**
* Normalizes and globally sets the configured options
*
* @return self
*/
protected function bakeOptions()
{
$this->options = A::nest($this->options, static::$nestIgnoreOptions);
Config::$data = $this->options;
return $this;
}
/**
@@ -223,7 +251,7 @@ class App
*/
protected function bakeRoots(array $roots = null)
{
$roots = array_merge(require static::$root . '/config/roots.php', (array)$roots);
$roots = array_merge(require dirname(__DIR__, 2) . '/config/roots.php', (array)$roots);
$this->roots = Ingredients::bake($roots);
return $this;
}
@@ -241,7 +269,7 @@ class App
$urls['index'] = $this->options['url'];
}
$urls = array_merge(require static::$root . '/config/urls.php', (array)$urls);
$urls = array_merge(require $this->root('kirby') . '/config/urls.php', (array)$urls);
$this->urls = Ingredients::bake($urls);
return $this;
}
@@ -285,11 +313,11 @@ class App
$router = $this->router();
$router::$beforeEach = function ($route, $path, $method) {
$this->trigger('route:before', $route, $path, $method);
$this->trigger('route:before', compact('route', 'path', 'method'));
};
$router::$afterEach = function ($route, $path, $method, $result) {
return $this->apply('route:after', $route, $path, $method, $result);
$router::$afterEach = function ($route, $path, $method, $result, $final) {
return $this->apply('route:after', compact('route', 'path', 'method', 'result', 'final'), 'result');
};
return $router->call($path ?? $this->path(), $method ?? $this->request()->method());
@@ -357,6 +385,31 @@ class App
return $this->options['content']['ignore'] ?? Dir::$ignore;
}
/**
* Generates a non-guessable token based on model
* data and a configured salt
*
* @param mixed $model Object to pass to the salt callback if configured
* @param string $value Model data to include in the generated token
* @return string
*/
public function contentToken($model, string $value): string
{
if (method_exists($model, 'root') === true) {
$default = $model->root();
} else {
$default = $this->root('content');
}
$salt = $this->option('content.salt', $default);
if (is_a($salt, 'Closure') === true) {
$salt = $salt($model);
}
return hash_hmac('sha1', $value, $salt);
}
/**
* Calls a page controller by name
* and with the given arguments
@@ -524,12 +577,17 @@ class App
* Returns the current App instance
*
* @param \Kirby\Cms\App $instance
* @return self
* @param bool $lazy If `true`, the instance is only returned if already existing
* @return self|null
*/
public static function instance(self $instance = null)
public static function instance(self $instance = null, bool $lazy = false)
{
if ($instance === null) {
return static::$instance ?? new static();
if ($lazy === true) {
return static::$instance;
} else {
return static::$instance ?? new static();
}
}
return static::$instance = $instance;
@@ -659,7 +717,7 @@ class App
$data['site'] = $data['site'] ?? $data['kirby']->site();
$data['parent'] = $data['parent'] ?? $data['site']->page();
return KirbyTags::parse($text, $data, $this->options, $this->extensions['hooks']);
return KirbyTags::parse($text, $data, $this->options, $this);
}
/**
@@ -673,7 +731,7 @@ class App
*/
public function kirbytext(string $text = null, array $data = [], bool $inline = false): string
{
$text = $this->apply('kirbytext:before', $text);
$text = $this->apply('kirbytext:before', compact('text'), 'text');
$text = $this->kirbytags($text, $data);
$text = $this->markdown($text, $inline);
@@ -681,7 +739,7 @@ class App
$text = $this->smartypants($text);
}
$text = $this->apply('kirbytext:after', $text);
$text = $this->apply('kirbytext:after', compact('text'), 'text');
return $text;
}
@@ -859,6 +917,27 @@ class App
// inject all last-minute options recursively
$this->options = array_replace_recursive($this->options, $options);
// update the system with changed options
if (
isset($options['debug']) === true ||
isset($options['whoops']) === true ||
isset($options['editor']) === true
) {
$this->handleErrors();
}
if (isset($options['debug']) === true) {
$this->api = null;
}
if (isset($options['home']) === true || isset($options['error']) === true) {
$this->site = null;
}
if (isset($options['slugs']) === true) {
$this->i18n();
}
}
return $this->options;
@@ -1091,7 +1170,7 @@ class App
}
$registry = $this->extensions('routes');
$system = (include static::$root . '/config/routes.php')($this);
$system = (include $this->root('kirby') . '/config/routes.php')($this);
$routes = array_merge($system['before'], $registry, $system['after']);
return $this->routes = $routes;
@@ -1312,12 +1391,15 @@ class App
* Trigger a hook by name
*
* @internal
* @param string $name
* @param mixed ...$arguments
* @param string $name Full event name
* @param array $args Associative array of named event arguments
* @param \Kirby\Cms\Event|null $originalEvent Event object (internal use)
* @return void
*/
public function trigger(string $name, ...$arguments)
public function trigger(string $name, array $args = [], ?Event $originalEvent = null)
{
$event = $originalEvent ?? new Event($name, $args);
if ($functions = $this->extension('hooks', $name)) {
static $level = 0;
static $triggered = [];
@@ -1332,7 +1414,7 @@ class App
$triggered[$name][] = $function;
// bind the App object to the hook
$function->call($this, ...$arguments);
$event->call($this, $function);
}
$level--;
@@ -1341,6 +1423,14 @@ class App
$triggered = [];
}
}
// trigger wildcard hooks if available
$nameWildcards = $event->nameWildcards();
if ($originalEvent === null && count($nameWildcards) > 0) {
foreach ($nameWildcards as $nameWildcard) {
$this->trigger($nameWildcard, $args, $event);
}
}
}
/**
@@ -1372,7 +1462,11 @@ class App
*/
public static function version(): ?string
{
return static::$version = static::$version ?? Data::read(static::$root . '/composer.json')['version'] ?? null;
try {
return static::$version = static::$version ?? Data::read(dirname(__DIR__, 2) . '/composer.json')['version'] ?? null;
} catch (Throwable $e) {
throw new LogicException('The Kirby version cannot be detected. The composer.json is probably missing or not readable.');
}
}
/**

View File

@@ -10,7 +10,7 @@ use Whoops\Handler\PrettyPageHandler;
use Whoops\Run as Whoops;
/**
* AppErrors
* PHP error handling using the Whoops library
*
* @package Kirby Cms
* @author Bastian Allgeier <bastian@getkirby.com>
@@ -20,14 +20,30 @@ use Whoops\Run as Whoops;
*/
trait AppErrors
{
/**
* Whoops instance cache
*
* @var \Whoops\Run
*/
protected $whoops;
/**
* Registers the PHP error handler for CLI usage
*
* @return void
*/
protected function handleCliErrors(): void
{
$whoops = new Whoops();
$whoops->pushHandler(new PlainTextHandler());
$whoops->register();
$this->setWhoopsHandler(new PlainTextHandler());
}
protected function handleErrors()
/**
* Registers the PHP error handler
* based on the environment
*
* @return void
*/
protected function handleErrors(): void
{
if ($this->request()->cli() === true) {
$this->handleCliErrors();
@@ -42,21 +58,25 @@ trait AppErrors
$this->handleHtmlErrors();
}
protected function handleHtmlErrors()
/**
* Registers the PHP error handler for HTML output
*
* @return void
*/
protected function handleHtmlErrors(): void
{
$whoops = new Whoops();
$handler = null;
if ($this->option('debug') === true) {
if ($this->option('whoops', true) === true) {
$handler = new PrettyPageHandler();
$handler->setPageTitle('Kirby CMS Debugger');
$handler->setResourcesPath(dirname(__DIR__, 2) . '/assets');
$handler->addCustomCss('whoops.css');
if ($editor = $this->option('editor')) {
$handler->setEditor($editor);
}
$whoops->pushHandler($handler);
$whoops->register();
}
} else {
$handler = new CallbackHandler(function ($exception, $inspector, $run) {
@@ -65,20 +85,27 @@ trait AppErrors
if (is_a($fatal, 'Closure') === true) {
echo $fatal($this);
} else {
include static::$root . '/views/fatal.php';
include $this->root('kirby') . '/views/fatal.php';
}
return Handler::QUIT;
});
}
$whoops->pushHandler($handler);
$whoops->register();
if ($handler !== null) {
$this->setWhoopsHandler($handler);
} else {
$this->unsetWhoopsHandler();
}
}
protected function handleJsonErrors()
/**
* Registers the PHP error handler for JSON output
*
* @return void
*/
protected function handleJsonErrors(): void
{
$whoops = new Whoops();
$handler = new CallbackHandler(function ($exception, $inspector, $run) {
if (is_a($exception, 'Kirby\Exception\Exception') === true) {
$httpCode = $exception->getHttpCode();
@@ -112,7 +139,46 @@ trait AppErrors
return Handler::QUIT;
});
$this->setWhoopsHandler($handler);
}
/**
* Enables Whoops with the specified handler
*
* @param Callable|\Whoops\Handler\HandlerInterface $handler
* @return void
*/
protected function setWhoopsHandler($handler): void
{
$whoops = $this->whoops();
$whoops->clearHandlers();
$whoops->pushHandler($handler);
$whoops->register();
$whoops->register(); // will only do something if not already registered
}
/**
* Clears the Whoops handlers and disables Whoops
*
* @return void
*/
protected function unsetWhoopsHandler(): void
{
$whoops = $this->whoops();
$whoops->clearHandlers();
$whoops->unregister(); // will only do something if currently registered
}
/**
* Returns the Whoops error handler instance
*
* @return \Whoops\Run
*/
protected function whoops()
{
if ($this->whoops !== null) {
return $this->whoops;
}
return $this->whoops = new Whoops();
}
}

View File

@@ -7,7 +7,7 @@ use Kirby\Exception\DuplicateException;
use Kirby\Form\Field as FormField;
use Kirby\Text\KirbyTag;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Collection;
use Kirby\Toolkit\Collection as ToolkitCollection;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\V;
@@ -30,6 +30,13 @@ trait AppPlugins
*/
protected static $plugins = [];
/**
* Cache for system extensions
*
* @var array
*/
protected static $systemExtensions = null;
/**
* The extension registry
*
@@ -47,6 +54,7 @@ trait AppPlugins
'components' => [],
'controllers' => [],
'collectionFilters' => [],
'collectionMethods' => [],
'fieldMethods' => [],
'fileMethods' => [],
'filesMethods' => [],
@@ -56,12 +64,14 @@ trait AppPlugins
'pageMethods' => [],
'pagesMethods' => [],
'pageModels' => [],
'permissions' => [],
'routes' => [],
'sections' => [],
'siteMethods' => [],
'snippets' => [],
'tags' => [],
'templates' => [],
'thirdParty' => [],
'translations' => [],
'userMethods' => [],
'userModels' => [],
@@ -69,13 +79,6 @@ trait AppPlugins
'validators' => []
];
/**
* Cache for system extensions
*
* @var array
*/
protected static $systemExtensions = null;
/**
* Flag when plugins have been loaded
* to not load them again
@@ -152,7 +155,18 @@ trait AppPlugins
*/
protected function extendCollectionFilters(array $filters): array
{
return $this->extensions['collectionFilters'] = Collection::$filters = array_merge(Collection::$filters, $filters);
return $this->extensions['collectionFilters'] = ToolkitCollection::$filters = array_merge(ToolkitCollection::$filters, $filters);
}
/**
* Registers additional collection methods
*
* @param array $methods
* @return array
*/
protected function extendCollectionMethods(array $methods): array
{
return $this->extensions['collectionMethods'] = Collection::$methods = array_merge(Collection::$methods, $methods);
}
/**
@@ -287,6 +301,10 @@ trait AppPlugins
$options = $prefixed;
}
// register each option in the nesting blacklist;
// this prevents Kirby from nesting the array keys inside each option
static::$nestIgnoreOptions = array_merge(static::$nestIgnoreOptions, array_keys($options));
return $this->extensions['options'] = $this->options = A::merge($options, $this->options, A::MERGE_REPLACE);
}
@@ -334,6 +352,22 @@ trait AppPlugins
return $this->extensions['pages'] = array_merge($this->extensions['pages'], $pages);
}
/**
* Registers additional permissions
*
* @param array $permissions
* @param \Kirby\Cms\Plugin|null $plugin
* @return array
*/
protected function extendPermissions(array $permissions, Plugin $plugin = null): array
{
if ($plugin !== null) {
$permissions = [$plugin->prefix() => $permissions];
}
return $this->extensions['permissions'] = Permissions::$extendedActions = array_merge(Permissions::$extendedActions, $permissions);
}
/**
* Registers additional routes
*
@@ -426,6 +460,20 @@ trait AppPlugins
return $this->extensions['translations'] = array_replace_recursive($this->extensions['translations'], $translations);
}
/**
* Add third party extensions to the registry
* so they can be used as plugins for plugins
* for example.
*
* @param string $type
* @param array $extensions
* @return array
*/
protected function extendThirdParty(array $extensions): array
{
return $this->extensions['thirdParty'] = array_replace_recursive($this->extensions['thirdParty'], $extensions);
}
/**
* Registers additional user methods
*
@@ -514,7 +562,7 @@ trait AppPlugins
$class = str_replace(['.', '-', '_'], '', $name) . 'Page';
// load the model class
include_once $model;
F::loadOnce($model);
if (class_exists($class) === true) {
$models[$name] = $class;
@@ -577,16 +625,18 @@ trait AppPlugins
*/
protected function extensionsFromSystem()
{
$root = $this->root('kirby');
// load static extensions only once
if (static::$systemExtensions === null) {
// Form Field Mixins
FormField::$mixins['filepicker'] = include static::$root . '/config/fields/mixins/filepicker.php';
FormField::$mixins['min'] = include static::$root . '/config/fields/mixins/min.php';
FormField::$mixins['options'] = include static::$root . '/config/fields/mixins/options.php';
FormField::$mixins['pagepicker'] = include static::$root . '/config/fields/mixins/pagepicker.php';
FormField::$mixins['picker'] = include static::$root . '/config/fields/mixins/picker.php';
FormField::$mixins['upload'] = include static::$root . '/config/fields/mixins/upload.php';
FormField::$mixins['userpicker'] = include static::$root . '/config/fields/mixins/userpicker.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';
// Tag Aliases
KirbyTag::$aliases = [
@@ -612,32 +662,32 @@ trait AppPlugins
];
// blueprint presets
PageBlueprint::$presets['pages'] = include static::$root . '/config/presets/pages.php';
PageBlueprint::$presets['page'] = include static::$root . '/config/presets/page.php';
PageBlueprint::$presets['files'] = include static::$root . '/config/presets/files.php';
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 static::$root . '/config/sections/mixins/empty.php';
Section::$mixins['headline'] = include static::$root . '/config/sections/mixins/headline.php';
Section::$mixins['help'] = include static::$root . '/config/sections/mixins/help.php';
Section::$mixins['layout'] = include static::$root . '/config/sections/mixins/layout.php';
Section::$mixins['max'] = include static::$root . '/config/sections/mixins/max.php';
Section::$mixins['min'] = include static::$root . '/config/sections/mixins/min.php';
Section::$mixins['pagination'] = include static::$root . '/config/sections/mixins/pagination.php';
Section::$mixins['parent'] = include static::$root . '/config/sections/mixins/parent.php';
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 static::$root . '/config/sections/info.php';
Section::$types['pages'] = include static::$root . '/config/sections/pages.php';
Section::$types['files'] = include static::$root . '/config/sections/files.php';
Section::$types['fields'] = include static::$root . '/config/sections/fields.php';
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 static::$root . '/config/components.php',
'blueprints' => include static::$root . '/config/blueprints.php',
'fields' => include static::$root . '/config/fields.php',
'fieldMethods' => include static::$root . '/config/methods.php',
'tags' => include static::$root . '/config/tags.php'
'components' => include $root . '/config/components.php',
'blueprints' => include $root . '/config/blueprints.php',
'fields' => include $root . '/config/fields.php',
'fieldMethods' => include $root . '/config/methods.php',
'tags' => include $root . '/config/tags.php'
];
}
@@ -656,6 +706,18 @@ trait AppPlugins
$this->extendTags(static::$systemExtensions['tags']);
}
/**
* Returns the native implementation
* of a core component
*
* @param string $component
* @return \Closure | false
*/
public function nativeComponent(string $component)
{
return static::$systemExtensions['components'][$component] ?? false;
}
/**
* Kirby plugin factory and getter
*
@@ -733,7 +795,7 @@ trait AppPlugins
continue;
}
include_once $entry;
F::loadOnce($entry);
$loaded[] = $dir;
}

View File

@@ -2,6 +2,7 @@
namespace Kirby\Cms;
use Closure;
use Throwable;
/**
@@ -36,12 +37,34 @@ trait AppUsers
/**
* Become any existing user
*
* @param string|null $who
* @return \Kirby\Cms\User|null
* @param string|null $who User ID or email address
* @param Closure|null $callback Optional action function that will be run with
* the permissions of the impersonated user; the
* impersonation will be reset afterwards
* @return mixed If called without callback: User that was impersonated;
* if called with callback: Return value from the callback
*/
public function impersonate(string $who = null)
public function impersonate(?string $who = null, ?Closure $callback = null)
{
return $this->auth()->impersonate($who);
$auth = $this->auth();
$userBefore = $auth->currentUserFromImpersonation();
$userAfter = $auth->impersonate($who);
if ($callback === null) {
return $userAfter;
}
try {
// bind the App object to the callback
return $callback->call($this, $userAfter);
} catch (Throwable $e) {
throw $e;
} finally {
// ensure that the impersonation is *always* reset
// to the original value, even if an error occurred
$auth->impersonate($userBefore !== null ? $userBefore->id() : null);
}
}
/**
@@ -77,20 +100,23 @@ trait AppUsers
* Returns a specific user by id
* or the current user if no id is given
*
* @param string $id
* @param string|null $id
* @param bool $allowImpersonation If set to false, only the actually
* logged in user will be returned
* (when `$id` is passed as `null`)
* @return \Kirby\Cms\User|null
*/
public function user(string $id = null)
public function user(?string $id = null, bool $allowImpersonation = true)
{
if ($id !== null) {
return $this->users()->find($id);
}
if (is_string($this->user) === true) {
if ($allowImpersonation === true && is_string($this->user) === true) {
return $this->auth()->impersonate($this->user);
} else {
try {
return $this->auth()->user();
return $this->auth()->user(null, $allowImpersonation);
} catch (Throwable $e) {
return null;
}

View File

@@ -85,6 +85,16 @@ class Auth
return $this->validatePassword($auth->username(), $auth->password());
}
/**
* Returns the currently impersonated user
*
* @return \Kirby\Cms\User|null
*/
public function currentUserFromImpersonation()
{
return $this->impersonate;
}
/**
* Returns the logged in user by checking
* the current session and finding a valid
@@ -124,10 +134,10 @@ class Auth
/**
* Become any existing user
*
* @param string|null $who
* @param string|null $who User ID or email address
* @return \Kirby\Cms\User|null
*/
public function impersonate(string $who = null)
public function impersonate(?string $who = null)
{
switch ($who) {
case null:
@@ -416,16 +426,19 @@ class Auth
/**
* Returns the current authentication type
*
* @param bool $allowImpersonation If set to false, 'impersonate' won't
* be returned as authentication type
* even if an impersonation is active
* @return string
*/
public function type(): string
public function type(bool $allowImpersonation = true): string
{
$basicAuth = $this->kirby->option('api.basicAuth', false);
$auth = $this->kirby->request()->auth();
if ($basicAuth === true && $auth && $auth->type() === 'basic') {
return 'basic';
} elseif ($this->impersonate !== null) {
} elseif ($allowImpersonation === true && $this->impersonate !== null) {
return 'impersonate';
} else {
return 'session';
@@ -436,13 +449,15 @@ class Auth
* Validates the currently logged in user
*
* @param \Kirby\Session\Session|array|null $session
* @param bool $allowImpersonation If set to false, only the actually
* logged in user will be returned
* @return \Kirby\Cms\User
*
* @throws \Throwable If an authentication error occured
*/
public function user($session = null)
public function user($session = null, bool $allowImpersonation = true)
{
if ($this->impersonate !== null) {
if ($allowImpersonation === true && $this->impersonate !== null) {
return $this->impersonate;
}

View File

@@ -45,7 +45,7 @@ class Collection extends BaseCollection
public function __call(string $key, $arguments)
{
// collection methods
if ($this->hasMethod($key)) {
if ($this->hasMethod($key) === true) {
return $this->callMethod($key, $arguments);
}
}

View File

@@ -4,6 +4,7 @@ namespace Kirby\Cms;
use Kirby\Exception\NotFoundException;
use Kirby\Toolkit\Controller;
use Kirby\Toolkit\F;
/**
* Manages and loads all collections
@@ -119,8 +120,8 @@ class Collections
// first check for collection file
$file = $kirby->root('collections') . '/' . $name . '.php';
if (file_exists($file)) {
$collection = require $file;
if (is_file($file) === true) {
$collection = F::load($file);
if (is_a($collection, 'Closure')) {
return $collection;

View File

@@ -257,6 +257,10 @@ class Content
public function update(array $content = null, bool $overwrite = false)
{
$this->data = $overwrite === true ? (array)$content : array_merge($this->data, (array)$content);
// clear cache of Field objects
$this->fields = [];
return $this;
}
}

View File

@@ -2,7 +2,7 @@
namespace Kirby\Cms;
use Kirby\Data\Yaml;
use Kirby\Data\Data;
use Kirby\Exception\Exception;
use Kirby\Toolkit\F;
@@ -108,7 +108,7 @@ class ContentLocks
// always read the whole file
rewind($handle);
$string = fread($handle, $filesize);
$data = Yaml::decode($string);
$data = Data::decode($string, 'yaml');
}
}
@@ -207,7 +207,7 @@ class ContentLocks
return F::remove($file);
}
$yaml = Yaml::encode($this->data[$file]);
$yaml = Data::encode($this->data[$file], 'yaml');
// delete all file contents first
if (rewind($handle) !== true || ftruncate($handle, 0) !== true) {

288
kirby/src/Cms/Event.php Executable file
View File

@@ -0,0 +1,288 @@
<?php
namespace Kirby\Cms;
use Closure;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\Controller;
/**
* The Event object is created whenever the `$kirby->trigger()`
* or `$kirby->apply()` methods are called. It collects all
* event information and handles calling the individual hooks.
*
* @package Kirby Cms
* @author Lukas Bestle <lukas@getkirby.com>,
* Ahmet Bora
* @link https://getkirby.com
* @copyright Bastian Allgeier GmbH
* @license https://getkirby.com/license
*/
class Event
{
/**
* The full event name
* (e.g. `page.create:after`)
*
* @var string
*/
protected $name;
/**
* The event type
* (e.g. `page` in `page.create:after`)
*
* @var string
*/
protected $type;
/**
* The event action
* (e.g. `create` in `page.create:after`)
*
* @var string|null
*/
protected $action;
/**
* The event state
* (e.g. `after` in `page.create:after`)
*
* @var string|null
*/
protected $state;
/**
* The event arguments
*
* @var array
*/
protected $arguments = [];
/**
* Class constructor
*
* @param string $name Full event name
* @param array $arguments Associative array of named event arguments
*/
public function __construct(string $name, array $arguments = [])
{
// split the event name into `$type.$action:$state`
// $action and $state are optional;
// if there is more than one dot, $type will be greedy
$regex = '/^(?<type>.+?)(?:\.(?<action>[^.]*?))?(?:\:(?<state>.*))?$/';
preg_match($regex, $name, $matches, PREG_UNMATCHED_AS_NULL);
$this->name = $name;
$this->type = $matches['type'];
$this->action = $matches['action'] ?? null;
$this->state = $matches['state'] ?? null;
$this->arguments = $arguments;
}
/**
* Magic caller for event arguments
*
* @param string $method
* @param array $arguments
* @return mixed
*/
public function __call(string $method, array $arguments = [])
{
return $this->argument($method);
}
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
return $this->toArray();
}
/**
* Makes it possible to simply echo
* or stringify the entire object
*
* @return string
*/
public function __toString(): string
{
return $this->toString();
}
/**
* Returns the action of the event (e.g. `create`)
* or `null` if the event name does not include an action
*
* @return string|null
*/
public function action(): ?string
{
return $this->action;
}
/**
* Returns a specific event argument
*
* @param string $name
* @return mixed
*/
public function argument(string $name)
{
if (isset($this->arguments[$name]) === true) {
return $this->arguments[$name];
}
return null;
}
/**
* Returns the arguments of the event
*
* @return array
*/
public function arguments(): array
{
return $this->arguments;
}
/**
* Calls a hook with the event data and returns
* the hook's return value
*
* @param object|null $bind Optional object to bind to the hook function
* @param \Closure $hook
* @return mixed
*/
public function call($bind = null, Closure $hook)
{
// collect the list of possible hook arguments
$data = $this->arguments();
$data['event'] = $this;
// magically call the hook with the arguments it requested
$hook = new Controller($hook);
return $hook->call($bind, $data);
}
/**
* Returns the full name of the event
*
* @return string
*/
public function name(): string
{
return $this->name;
}
/**
* Returns the full list of possible wildcard
* event names based on the current event name
*
* @return array
*/
public function nameWildcards(): array
{
// if the event is already a wildcard event, no further variation is possible
if ($this->type === '*' || $this->action === '*' || $this->state === '*') {
return [];
}
if ($this->action !== null && $this->state !== null) {
// full $type.$action:$state event
return [
$this->type . '.*:' . $this->state,
$this->type . '.' . $this->action . ':*',
$this->type . '.*:*',
'*.' . $this->action . ':' . $this->state,
'*.' . $this->action . ':*',
'*:' . $this->state,
'*'
];
} elseif ($this->state !== null) {
// event without action: $type:$state
return [
$this->type . ':*',
'*:' . $this->state,
'*'
];
} elseif ($this->action !== null) {
// event without state: $type.$action
return [
$this->type . '.*',
'*.' . $this->action,
'*'
];
} else {
// event with a simple name
return ['*'];
}
}
/**
* Returns the state of the event (e.g. `after`)
*
* @return string|null
*/
public function state(): ?string
{
return $this->state;
}
/**
* Returns the event data as array
*
* @return array
*/
public function toArray(): array
{
return [
'name' => $this->name,
'arguments' => $this->arguments
];
}
/**
* Returns the event name as string
*
* @return string
*/
public function toString(): string
{
return $this->name;
}
/**
* Returns the type of the event (e.g. `page`)
*
* @return string
*/
public function type(): string
{
return $this->type;
}
/**
* Updates a given argument with a new value
*
* @internal
* @param string $name
* @param mixed $value
* @return void
*/
public function updateArgument(string $name, $value): void
{
if (array_key_exists($name, $this->arguments) !== true) {
throw new InvalidArgumentException('The argument ' . $name . ' does not exist');
}
$this->arguments[$name] = $value;
}
}

View File

@@ -326,14 +326,32 @@ class File extends ModelWithContent
}
/**
* Create a unique media hash
* Check if the file can be read by the current user
*
* @return bool
*/
public function isReadable(): bool
{
static $readable = [];
$template = $this->template();
if (isset($readable[$template]) === true) {
return $readable[$template];
}
return $readable[$template] = $this->permissions()->can('read');
}
/**
* Creates a unique media hash
*
* @internal
* @return string
*/
public function mediaHash(): string
{
return crc32($this->filename()) . '-' . $this->modifiedFile();
return $this->mediaToken() . '-' . $this->modifiedFile();
}
/**
@@ -347,6 +365,18 @@ class File extends ModelWithContent
return $this->parent()->mediaRoot() . '/' . $this->mediaHash() . '/' . $this->filename();
}
/**
* Creates a non-guessable token string for this file
*
* @internal
* @return string
*/
public function mediaToken(): string
{
$token = $this->kirby()->contentToken($this, $this->id());
return substr($token, 0, 10);
}
/**
* Returns the absolute Url to the file in the public media folder
*
@@ -375,12 +405,13 @@ class File extends ModelWithContent
*
* @param string $format
* @param string|null $handler date or strftime
* @param string|null $languageCode
* @return mixed
*/
public function modified(string $format = null, string $handler = null)
public function modified(string $format = null, string $handler = null, string $languageCode = null)
{
$file = $this->modifiedFile();
$content = $this->modifiedContent();
$content = $this->modifiedContent($languageCode);
$modified = max($file, $content);
if (is_null($format) === true) {
@@ -396,11 +427,12 @@ class File extends ModelWithContent
* Timestamp of the last modification
* of the content file
*
* @param string|null $languageCode
* @return int
*/
protected function modifiedContent(): int
protected function modifiedContent(string $languageCode = null): int
{
return F::modified($this->contentFile());
return F::modified($this->contentFile($languageCode));
}
/**

View File

@@ -38,7 +38,7 @@ trait FileActions
return $this;
}
return $this->commit('changeName', [$this, $name], function ($oldFile, $name) {
return $this->commit('changeName', ['file' => $this, 'name' => $name], function ($oldFile, $name) {
$newFile = $oldFile->clone([
'filename' => $name . '.' . $oldFile->extension(),
]);
@@ -87,7 +87,7 @@ trait FileActions
*/
public function changeSort(int $sort)
{
return $this->commit('changeSort', [$this, $sort], function ($file, $sort) {
return $this->commit('changeSort', ['file' => $this, 'position' => $sort], function ($file, $sort) {
return $file->save(['sort' => $sort]);
});
}
@@ -108,13 +108,24 @@ trait FileActions
*/
protected function commit(string $action, array $arguments, Closure $callback)
{
$old = $this->hardcopy();
$kirby = $this->kirby();
$old = $this->hardcopy();
$kirby = $this->kirby();
$argumentValues = array_values($arguments);
$this->rules()->$action(...$argumentValues);
$kirby->trigger('file.' . $action . ':before', $arguments);
$result = $callback(...$argumentValues);
if ($action === 'create') {
$argumentsAfter = ['file' => $result];
} elseif ($action === 'delete') {
$argumentsAfter = ['status' => $result, 'file' => $old];
} else {
$argumentsAfter = ['newFile' => $result, 'oldFile' => $old];
}
$kirby->trigger('file.' . $action . ':after', $argumentsAfter);
$this->rules()->$action(...$arguments);
$kirby->trigger('file.' . $action . ':before', ...$arguments);
$result = $callback(...$arguments);
$kirby->trigger('file.' . $action . ':after', $result, $old);
$kirby->cache('pages')->flush();
return $result;
}
@@ -175,7 +186,7 @@ trait FileActions
$file = $file->clone(['content' => $form->strings(true)]);
// run the hook
return $file->commit('create', [$file, $upload], function ($file, $upload) {
return $file->commit('create', compact('file', 'upload'), function ($file, $upload) {
// delete all public versions
$file->unpublish();
@@ -211,7 +222,7 @@ trait FileActions
*/
public function delete(): bool
{
return $this->commit('delete', [$this], function ($file) {
return $this->commit('delete', ['file' => $this], function ($file) {
// remove all versions in the media folder
$file->unpublish();
@@ -243,7 +254,7 @@ trait FileActions
*/
public function publish()
{
Media::publish($this->root(), $this->mediaRoot());
Media::publish($this, $this->mediaRoot());
return $this;
}
@@ -273,7 +284,7 @@ trait FileActions
*/
public function replace(string $source)
{
return $this->commit('replace', [$this, new Image($source)], function ($file, $upload) {
return $this->commit('replace', ['file' => $this, 'upload' => new Image($source)], function ($file, $upload) {
// delete all public versions
$file->unpublish();
@@ -295,7 +306,7 @@ trait FileActions
*/
public function unpublish()
{
Media::unpublish($this->parent()->mediaRoot(), $this->filename());
Media::unpublish($this->parent()->mediaRoot(), $this);
return $this;
}
}

View File

@@ -26,6 +26,7 @@ class FileBlueprint extends Blueprint
'changeName' => null,
'create' => null,
'delete' => null,
'read' => null,
'replace' => null,
'update' => null,
]

View File

@@ -298,6 +298,6 @@ class Filename
'name' => $this->name(),
'attributes' => $this->attributesToString('-'),
'extension' => $this->extension()
]);
], '');
}
}

View File

@@ -55,7 +55,7 @@ class Files extends Collection
* Sort all given files by the
* order in the array
*
* @param array $files List of filenames
* @param array $files List of file ids
* @param int $offset Sorting offset
* @return self
*/

View File

@@ -126,7 +126,7 @@ trait HasChildren
* Finds one or multiple children by id
*
* @param string ...$arguments
* @return \Kirby\Cms\Page|\Kirby\Cms\Pages
* @return \Kirby\Cms\Page|\Kirby\Cms\Pages|null
*/
public function find(...$arguments)
{

View File

@@ -2,6 +2,8 @@
namespace Kirby\Cms;
use Kirby\Exception\BadMethodCallException;
/**
* HasMethods
*
@@ -31,7 +33,13 @@ trait HasMethods
*/
public function callMethod(string $method, array $args = [])
{
return static::$methods[$method]->call($this, ...$args);
$closure = $this->getMethod($method);
if ($closure === null) {
throw new BadMethodCallException('The method ' . $method . ' does not exist');
}
return $closure->call($this, ...$args);
}
/**
@@ -43,6 +51,29 @@ trait HasMethods
*/
public function hasMethod(string $method): bool
{
return isset(static::$methods[$method]) === true;
return $this->getMethod($method) !== null;
}
/**
* Returns a registered method by name, either from
* the current class or from a parent class ordered by
* inheritance order (top to bottom)
*
* @param string $method
* @return Closure|null
*/
protected function getMethod(string $method)
{
if (isset(static::$methods[$method]) === true) {
return static::$methods[$method];
}
foreach (class_parents($this) as $parent) {
if (isset($parent::$methods[$method]) === true) {
return $parent::$methods[$method];
}
}
return null;
}
}

View File

@@ -25,32 +25,19 @@ class KirbyTags extends \Kirby\Text\KirbyTags
* @param string $text
* @param array $data
* @param array $options
* @param array $hooks
* @param \Kirby\Cms\App $app
* @return string
*/
public static function parse(string $text = null, array $data = [], array $options = [], array $hooks = []): string
public static function parse(string $text = null, array $data = [], array $options = [], ?App $app = null): string
{
$text = static::hooks($hooks['kirbytags:before'] ?? [], $text, $data, $options);
if ($app !== null) {
$text = $app->apply('kirbytags:before', compact('text', 'data', 'options'), 'text');
}
$text = parent::parse($text, $data, $options);
$text = static::hooks($hooks['kirbytags:after'] ?? [], $text, $data, $options);
return $text;
}
/**
* Runs the given hooks and returns the
* modified text
*
* @param array $hooks
* @param string $text
* @param array $data
* @param array $options
* @return string|null
*/
protected static function hooks(array $hooks, string $text = null, array $data, array $options): ?string
{
foreach ($hooks as $hook) {
$text = $hook->call($data['kirby'], $text, $data, $options);
if ($app !== null) {
$text = $app->apply('kirbytags:after', compact('text', 'data', 'options'), 'text');
}
return $text;

View File

@@ -94,7 +94,7 @@ class Languages extends Collection
$files = glob(App::instance()->root('languages') . '/*.php');
foreach ($files as $file) {
$props = include $file;
$props = F::load($file);
if (is_array($props) === true) {
// inject the language code from the filename if it does not exist

View File

@@ -5,6 +5,7 @@ namespace Kirby\Cms;
use Kirby\Data\Data;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Str;
use Throwable;
/**
@@ -41,9 +42,15 @@ class Media
// this should work for all original files
if ($file = $model->file($filename)) {
// the media hash is outdated. redirect to the correct url
// check if the request contained an outdated media hash
if ($file->mediaHash() !== $hash) {
return Response::redirect($file->mediaUrl(), 307);
// if at least the token was correct, redirect
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);
}
}
// send the file to the browser
@@ -57,18 +64,18 @@ class Media
/**
* Copy the file to the final media folder location
*
* @param string $src
* @param \Kirby\Cms\File $file
* @param string $dest
* @return bool
*/
public static function publish(string $src, string $dest): bool
public static function publish(File $file, string $dest): bool
{
$filename = basename($src);
$src = $file->root();
$version = dirname($dest);
$directory = dirname($version);
// unpublish all files except stuff in the version folder
Media::unpublish($directory, $filename, $version);
Media::unpublish($directory, $file, $version);
// copy/overwrite the file to the dest folder
return F::copy($src, $dest, true);
@@ -125,21 +132,25 @@ class Media
}
/**
* Deletes all versions of the given filename
* Deletes all versions of the given file
* within the parent directory
*
* @param string $directory
* @param string $filename
* @param \Kirby\Cms\File $file
* @param string $ignore
* @return bool
*/
public static function unpublish(string $directory, string $filename, string $ignore = null): bool
public static function unpublish(string $directory, File $file, string $ignore = null): bool
{
if (is_dir($directory) === false) {
return true;
}
$versions = glob($directory . '/' . crc32($filename) . '*', GLOB_ONLYDIR);
// get both old and new versions (pre and post Kirby 3.4.0)
$versions = array_merge(
glob($directory . '/' . crc32($file->filename()) . '-*', GLOB_ONLYDIR),
glob($directory . '/' . $file->mediaToken() . '-*', GLOB_ONLYDIR)
);
// delete all versions of the file
foreach ($versions as $version) {

View File

@@ -334,6 +334,11 @@ abstract class ModelWithContent extends Model
}
if (is_string($settings) === true) {
// use defined icon in blueprint
if ($settings === 'icon') {
return [];
}
$settings = [
'query' => $settings
];
@@ -448,11 +453,15 @@ abstract class ModelWithContent extends Model
return null;
}
$result = Str::query($query, [
'kirby' => $this->kirby(),
'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(),
static::CLASS_ALIAS => $this
]);
try {
$result = Str::query($query, [
'kirby' => $this->kirby(),
'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(),
static::CLASS_ALIAS => $this
]);
} catch (Throwable $e) {
return null;
}
if ($expect !== null && is_a($result, $expect) !== true) {
return null;
@@ -611,19 +620,21 @@ abstract class ModelWithContent extends Model
* String template builder
*
* @param string|null $template
* @param array $data
* @param string $fallback Fallback for tokens in the template that cannot be replaced
* @return string
*/
public function toString(string $template = null): string
public function toString(string $template = null, array $data = [], string $fallback = ''): string
{
if ($template === null) {
return $this->id();
}
$result = Str::template($template, [
$result = Str::template($template, array_replace([
'kirby' => $this->kirby(),
'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(),
static::CLASS_ALIAS => $this
]);
], $data), $fallback);
return $result;
}
@@ -691,7 +702,8 @@ abstract class ModelWithContent extends Model
}
}
return $this->commit('update', [$this, $form->data(), $form->strings(), $languageCode], function ($model, $values, $strings, $languageCode) {
$arguments = [static::CLASS_ALIAS => $this, 'values' => $form->data(), 'strings' => $form->strings(), 'languageCode' => $languageCode];
return $this->commit('update', $arguments, function ($model, $values, $strings, $languageCode) {
// save updated values
$model = $model->save($strings, $languageCode, true);

View File

@@ -3,6 +3,7 @@
namespace Kirby\Cms;
use Kirby\Exception\Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Http\Uri;
use Kirby\Toolkit\A;
@@ -344,7 +345,31 @@ class Page extends ModelWithContent
]);
// call the template controller if there's one.
return array_merge($kirby->controller($this->template()->name(), $data, $contentType), $data);
$controllerData = $kirby->controller($this->template()->name(), $data, $contentType);
// merge controller data with original data safely
if (empty($controllerData) === false) {
$classes = [
'kirby' => 'Kirby\Cms\App',
'site' => 'Kirby\Cms\Site',
'pages' => 'Kirby\Cms\Pages',
'page' => 'Kirby\Cms\Page'
];
foreach ($controllerData as $key => $value) {
if (array_key_exists($key, $classes) === true) {
if (is_a($value, $classes[$key]) === true) {
$data[$key] = $value;
} else {
throw new InvalidArgumentException('The returned variable "' . $key . '" from the controller "' . $this->template()->name() . '" is not of the required type "' . $classes[$key] . '"');
}
} else {
$data[$key] = $value;
}
}
}
return $data;
}
/**
@@ -453,6 +478,20 @@ class Page extends ModelWithContent
return new static($props);
}
/**
* Redirects to this page,
* wrapper for the `go()` helper
*
* @since 3.4.0
*
* @param array $options Options for `Kirby\Http\Uri` to create URL parts
* @param int $code HTTP status code
*/
public function go(array $options = [], int $code = 302)
{
go($this->url($options), $code);
}
/**
* Checks if the intended template
* for the page exists.
@@ -889,11 +928,16 @@ class Page extends ModelWithContent
*
* @param string $format
* @param string|null $handler
* @param string|null $languageCode
* @return int|string
*/
public function modified(string $format = null, string $handler = null)
public function modified(string $format = null, string $handler = null, string $languageCode = null)
{
return F::modified($this->contentFile(), $format, $handler ?? $this->kirby()->option('date.handler', 'date'));
return F::modified(
$this->contentFile($languageCode),
$format,
$handler ?? $this->kirby()->option('date.handler', 'date')
);
}
/**
@@ -1448,7 +1492,7 @@ class Page extends ModelWithContent
*/
protected function token(): string
{
return sha1($this->id() . $this->template());
return $this->kirby()->contentToken($this, $this->id() . $this->template());
}
/**

View File

@@ -42,7 +42,7 @@ trait PageActions
return $this;
}
return $this->commit('changeNum', [$this, $num], function ($oldPage, $num) {
return $this->commit('changeNum', ['page' => $this, 'num' => $num], function ($oldPage, $num) {
$newPage = $oldPage->clone([
'num' => $num,
'dirname' => null,
@@ -97,7 +97,8 @@ trait PageActions
return $this;
}
return $this->commit('changeSlug', [$this, $slug, $languageCode = null], function ($oldPage, $slug) {
$arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => null];
return $this->commit('changeSlug', $arguments, function ($oldPage, $slug) {
$newPage = $oldPage->clone([
'slug' => $slug,
'dirname' => null,
@@ -151,7 +152,8 @@ trait PageActions
throw new InvalidArgumentException('Use the changeSlug method to change the slug for the default language');
}
return $this->commit('changeSlug', [$this, $slug, $languageCode], function ($page, $slug, $languageCode) {
$arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $languageCode];
return $this->commit('changeSlug', $arguments, function ($page, $slug, $languageCode) {
// remove the slug if it's the same as the folder name
if ($slug === $page->uid()) {
$slug = null;
@@ -185,7 +187,8 @@ trait PageActions
protected function changeStatusToDraft()
{
$page = $this->commit('changeStatus', [$this, 'draft', null], function ($page) {
$arguments = ['page' => $this, 'status' => 'draft', 'position' => null];
$page = $this->commit('changeStatus', $arguments, function ($page) {
return $page->unpublish();
});
@@ -206,7 +209,8 @@ trait PageActions
return $this;
}
$page = $this->commit('changeStatus', [$this, 'listed', $num], function ($page, $status, $position) {
$arguments = ['page' => $this, 'status' => 'listed', 'position' => $num];
$page = $this->commit('changeStatus', $arguments, function ($page, $status, $position) {
return $page->publish()->changeNum($position);
});
@@ -226,7 +230,8 @@ trait PageActions
return $this;
}
$page = $this->commit('changeStatus', [$this, 'unlisted', null], function ($page) {
$arguments = ['page' => $this, 'status' => 'unlisted', 'position' => null];
$page = $this->commit('changeStatus', $arguments, function ($page) {
return $page->publish()->changeNum(null);
});
@@ -243,11 +248,11 @@ trait PageActions
*/
public function changeTemplate(string $template)
{
if ($template === $this->template()->name()) {
if ($template === $this->intendedTemplate()->name()) {
return $this;
}
return $this->commit('changeTemplate', [$this, $template], function ($oldPage, $template) {
return $this->commit('changeTemplate', ['page' => $this, 'template' => $template], function ($oldPage, $template) {
if ($this->kirby()->multilang() === true) {
$newPage = $this->clone([
'template' => $template
@@ -290,7 +295,8 @@ trait PageActions
*/
public function changeTitle(string $title, string $languageCode = null)
{
return $this->commit('changeTitle', [$this, $title, $languageCode], function ($page, $title, $languageCode) {
$arguments = ['page' => $this, 'title' => $title, 'languageCode' => $languageCode];
return $this->commit('changeTitle', $arguments, function ($page, $title, $languageCode) {
return $page->save(['title' => $title], $languageCode);
});
}
@@ -311,13 +317,27 @@ trait PageActions
*/
protected function commit(string $action, array $arguments, Closure $callback)
{
$old = $this->hardcopy();
$old = $this->hardcopy();
$kirby = $this->kirby();
$argumentValues = array_values($arguments);
$this->rules()->$action(...$arguments);
$this->kirby()->trigger('page.' . $action . ':before', ...$arguments);
$result = $callback(...$arguments);
$this->kirby()->trigger('page.' . $action . ':after', $result, $old);
$this->kirby()->cache('pages')->flush();
$this->rules()->$action(...$argumentValues);
$kirby->trigger('page.' . $action . ':before', $arguments);
$result = $callback(...$argumentValues);
if ($action === 'create') {
$argumentsAfter = ['page' => $result];
} elseif ($action === 'duplicate') {
$argumentsAfter = ['duplicatePage' => $result];
} elseif ($action === 'delete') {
$argumentsAfter = ['status' => $result, 'page' => $old];
} else {
$argumentsAfter = ['newPage' => $result, 'oldPage' => $old];
}
$kirby->trigger('page.' . $action . ':after', $argumentsAfter);
$kirby->cache('pages')->flush();
return $result;
}
@@ -356,7 +376,9 @@ trait PageActions
'slug' => $slug,
]);
$ignore = [];
$ignore = [
$this->kirby()->locks()->file($this)
];
// don't copy files
if ($files === false) {
@@ -375,7 +397,7 @@ trait PageActions
// remove all translated slugs
if ($this->kirby()->multilang() === true) {
foreach ($this->kirby()->languages() as $language) {
if ($language->isDefault() === false) {
if ($language->isDefault() === false && $copy->translation($language)->exists() === true) {
$copy = $copy->save(['slug' => null], $language->code());
}
}
@@ -416,7 +438,7 @@ trait PageActions
$page = $page->clone(['content' => $form->strings(true)]);
// run the hooks and creation action
$page = $page->commit('create', [$page, $props], function ($page, $props) {
$page = $page->commit('create', ['page' => $page, 'input' => $props], function ($page, $props) {
// always create pages in the default language
if ($page->kirby()->multilang() === true) {
@@ -461,7 +483,8 @@ trait PageActions
'site' => $this->site(),
]);
return static::create($props);
$modelClass = Page::$models[$props['template']] ?? Page::class;
return $modelClass::create($props);
}
/**
@@ -520,7 +543,7 @@ trait PageActions
'kirby' => $app,
'page' => $app->page($this->id()),
'site' => $app->site(),
]);
], '');
return (int)$template;
}
@@ -534,7 +557,7 @@ trait PageActions
*/
public function delete(bool $force = false): bool
{
return $this->commit('delete', [$this, $force], function ($page, $force) {
return $this->commit('delete', ['page' => $this, 'force' => $force], function ($page, $force) {
// delete all files individually
foreach ($page->files() as $file) {
@@ -591,7 +614,8 @@ trait PageActions
// create the slug for the duplicate
$slug = Str::slug($slug ?? $this->slug() . '-copy');
return $this->commit('duplicate', [$this, $slug, $options], function ($page, $slug, $options) {
$arguments = ['originalPage' => $this, 'input' => $slug, 'options' => $options];
return $this->commit('duplicate', $arguments, function ($page, $slug, $options) {
return $this->copy([
'parent' => $this->parent(),
'slug' => $slug,

View File

@@ -238,11 +238,11 @@ class Pages extends Collection
public function findByIdRecursive(string $id, string $startAt = null, bool $multiLang = false)
{
$path = explode('/', $id);
$collection = $this;
$item = null;
$query = $startAt;
foreach ($path as $key) {
$collection = $item ? $item->children() : $this;
$query = ltrim($query . '/' . $key, '/');
$item = $collection->get($query) ?? null;
@@ -253,8 +253,6 @@ class Pages extends Collection
if ($item === null) {
return null;
}
$collection = $item->children();
}
return $item;
@@ -340,9 +338,12 @@ class Pages extends Collection
foreach ($this->data as $pageKey => $page) {
$this->index->data[$pageKey] = $page;
$index = $page->index($drafts);
foreach ($page->index($drafts) as $childKey => $child) {
$this->index->data[$childKey] = $child;
if ($index) {
foreach ($index as $childKey => $child) {
$this->index->data[$childKey] = $child;
}
}
}

View File

@@ -23,7 +23,7 @@ use Kirby\Toolkit\Pagination as BasePagination;
class Pagination extends BasePagination
{
/**
* Pagination method (param or query)
* Pagination method (param, query, none)
*
* @var string
*/
@@ -82,7 +82,7 @@ class Pagination extends BasePagination
if ($params['method'] === 'query') {
$params['page'] = $params['page'] ?? $params['url']->query()->get($params['variable']);
} else {
} elseif ($params['method'] === 'param') {
$params['page'] = $params['page'] ?? $params['url']->params()->get($params['variable']);
}
@@ -153,8 +153,10 @@ class Pagination extends BasePagination
if ($this->method === 'query') {
$url->query->$variable = $pageValue;
} else {
} elseif ($this->method === 'param') {
$url->params->$variable = $pageValue;
} else {
return null;
}
return $url->toString();

View File

@@ -89,7 +89,7 @@ class Panel
go($kirby->url('index') . '/' . $kirby->path());
}
} catch (Throwable $e) {
die('The panel assets cannot be installed properly. Please check permissions of your media folder.');
die('The Panel assets cannot be installed properly. ' . $e->getMessage());
}
// get the uri object for the panel url

View File

@@ -17,6 +17,8 @@ use Kirby\Exception\InvalidArgumentException;
*/
class Permissions
{
public static $extendedActions = [];
protected $actions = [
'access' => [
'panel' => true,
@@ -28,6 +30,7 @@ class Permissions
'changeName' => true,
'create' => true,
'delete' => true,
'read' => true,
'replace' => true,
'update' => true
],
@@ -75,6 +78,15 @@ class Permissions
public function __construct($settings = [])
{
// dynamically register the extended actions
foreach (static::$extendedActions as $key => $actions) {
if (isset($this->actions[$key]) === true) {
throw new InvalidArgumentException('The action ' . $key . ' is already a core action');
}
$this->actions[$key] = $actions;
}
if (is_array($settings) === true) {
return $this->setCategories($settings);
}

View File

@@ -2,8 +2,6 @@
namespace Kirby\Cms;
use Kirby\Toolkit\Str;
/**
* The Search class extracts the
* search logic from collections, to
@@ -31,100 +29,15 @@ class Search
/**
* Native search method to search for anything within the collection
*
* @param Collection $collection
* @param \Kirby\Cms\Collection $collection
* @param string $query
* @param mixed $params
* @return \Kirby\Cms\Collection|bool
*/
public static function collection(Collection $collection, string $query = null, $params = [])
{
if (empty(trim($query)) === true) {
return $collection->limit(0);
}
if (is_string($params) === true) {
$params = ['fields' => Str::split($params, '|')];
}
$defaults = [
'fields' => [],
'minlength' => 2,
'score' => [],
'words' => false,
];
$options = array_merge($defaults, $params);
$collection = clone $collection;
$searchwords = preg_replace('/(\s)/u', ',', $query);
$searchwords = Str::split($searchwords, ',', $options['minlength']);
$lowerQuery = mb_strtolower($query);
if (empty($options['stopwords']) === false) {
$searchwords = array_diff($searchwords, $options['stopwords']);
}
$searchwords = array_map(function ($value) use ($options) {
return $options['words'] ? '\b' . preg_quote($value) . '\b' : preg_quote($value);
}, $searchwords);
$preg = '!(' . implode('|', $searchwords) . ')!i';
$results = $collection->filter(function ($item) use ($query, $preg, $options, $lowerQuery) {
$data = $item->content()->toArray();
$keys = array_keys($data);
$keys[] = 'id';
if (is_a($item, 'Kirby\Cms\User') === true) {
$keys[] = 'name';
$keys[] = 'email';
$keys[] = 'role';
} elseif (is_a($item, 'Kirby\Cms\Page') === true) {
// apply the default score for pages
$options['score'] = array_merge([
'id' => 64,
'title' => 64,
], $options['score']);
}
if (empty($options['fields']) === false) {
$fields = array_map('strtolower', $options['fields']);
$keys = array_intersect($keys, $fields);
}
$item->searchHits = 0;
$item->searchScore = 0;
foreach ($keys as $key) {
$score = $options['score'][$key] ?? 1;
$value = $data[$key] ?? (string)$item->$key();
$lowerValue = mb_strtolower($value);
// check for exact matches
if ($lowerQuery == $lowerValue) {
$item->searchScore += 16 * $score;
$item->searchHits += 1;
// check for exact beginning matches
} elseif (Str::startsWith($lowerValue, $lowerQuery) === true) {
$item->searchScore += 8 * $score;
$item->searchHits += 1;
// check for exact query matches
} elseif ($matches = preg_match_all('!' . preg_quote($query) . '!i', $value, $r)) {
$item->searchScore += 2 * $score;
$item->searchHits += $matches;
}
// check for any match
if ($matches = preg_match_all($preg, $value, $r)) {
$item->searchHits += $matches;
$item->searchScore += $matches * $score;
}
}
return $item->searchHits > 0 ? true : false;
});
return $results->sortBy('searchScore', 'desc');
$kirby = App::instance();
return $kirby->component('search')($kirby, $collection, $query, $params);
}
/**

View File

@@ -31,13 +31,17 @@ trait SiteActions
*/
protected function commit(string $action, array $arguments, Closure $callback)
{
$old = $this->hardcopy();
$kirby = $this->kirby();
$old = $this->hardcopy();
$kirby = $this->kirby();
$argumentValues = array_values($arguments);
$this->rules()->$action(...$argumentValues);
$kirby->trigger('site.' . $action . ':before', $arguments);
$result = $callback(...$argumentValues);
$kirby->trigger('site.' . $action . ':after', ['newSite' => $result, 'oldSite' => $old]);
$this->rules()->$action(...$arguments);
$kirby->trigger('site.' . $action . ':before', ...$arguments);
$result = $callback(...$arguments);
$kirby->trigger('site.' . $action . ':after', $result, $old);
$kirby->cache('pages')->flush();
return $result;
}
@@ -51,7 +55,8 @@ trait SiteActions
*/
public function changeTitle(string $title, string $languageCode = null)
{
return $this->commit('changeTitle', [$this, $title, $languageCode], function ($site, $title, $languageCode) {
$arguments = ['site' => $this, 'title' => $title, 'languageCode' => $languageCode];
return $this->commit('changeTitle', $arguments, function ($site, $title, $languageCode) {
return $site->save(['title' => $title], $languageCode);
});
}

View File

@@ -9,6 +9,7 @@ use Kirby\Exception\PermissionException;
use Kirby\Http\Remote;
use Kirby\Http\Uri;
use Kirby\Http\Url;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Str;
@@ -281,7 +282,9 @@ class System
* Loads the license file and returns
* the license information if available
*
* @return string|false
* @return string|bool License key or `false` if the current user has
* permissions for access.settings, otherwise just a
* boolean that tells whether a valid license is active
*/
public function license()
{
@@ -326,7 +329,14 @@ class System
return false;
}
return $license['license'];
// 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) {
return $license['license'];
} else {
return true;
}
}
/**
@@ -422,13 +432,17 @@ class System
*/
public function server(): bool
{
$servers = [
'apache',
'caddy',
'litespeed',
'nginx',
'php'
];
if ($servers = $this->app->option('servers')) {
$servers = A::wrap($servers);
} else {
$servers = [
'apache',
'caddy',
'litespeed',
'nginx',
'php'
];
}
$software = $_SERVER['SERVER_SOFTWARE'] ?? null;

View File

@@ -3,7 +3,6 @@
namespace Kirby\Cms;
use Kirby\Http\Url as BaseUrl;
use Kirby\Toolkit\Str;
/**
* The `Url` class extends the
@@ -61,36 +60,10 @@ class Url extends BaseUrl
*/
public static function to(string $path = null, $options = null): string
{
$kirby = App::instance();
$language = null;
$kirby = App::instance();
// get language from simple string option
if (is_string($options) === true) {
$language = $options;
$options = null;
}
// get language from array
if (is_array($options) === true && isset($options['language']) === true) {
$language = $options['language'];
unset($options['language']);
}
// get a language url for the linked page, if the page can be found
if ($kirby->multilang() === true) {
$parts = Str::split($path, '#');
if ($page = page($parts[0] ?? null)) {
$path = $page->url($language);
if (isset($parts[1]) === true) {
$path .= '#' . $parts[1];
}
}
}
return $kirby->component('url')($kirby, $path, $options, function (string $path = null, $options = null) {
return parent::to($path, $options);
return $kirby->component('url')($kirby, $path, $options, function (string $path = null, $options = null) use ($kirby) {
return $kirby->nativeComponent('url')($kirby, $path, $options);
});
}
}

View File

@@ -426,13 +426,13 @@ class User extends ModelWithContent
$session = $this->sessionFromOptions($session);
$kirby->trigger('user.login:before', $this, $session);
$kirby->trigger('user.login:before', ['user' => $this, 'session' => $session]);
$session->regenerateToken(); // privilege change
$session->data()->set('user.id', $this->id());
$this->kirby()->auth()->setUser($this);
$kirby->trigger('user.login:after', $this, $session);
$kirby->trigger('user.login:after', ['user' => $this, 'session' => $session]);
}
/**
@@ -446,7 +446,7 @@ class User extends ModelWithContent
$kirby = $this->kirby();
$session = $this->sessionFromOptions($session);
$kirby->trigger('user.logout:before', $this, $session);
$kirby->trigger('user.logout:before', ['user' => $this, 'session' => $session]);
// remove the user from the session for future requests
$session->data()->remove('user.id');
@@ -458,12 +458,12 @@ class User extends ModelWithContent
// session is now empty, we might as well destroy it
$session->destroy();
$kirby->trigger('user.logout:after', $this, null);
$kirby->trigger('user.logout:after', ['user' => $this, 'session' => null]);
} else {
// privilege change
$session->regenerateToken();
$kirby->trigger('user.logout:after', $this, $session);
$kirby->trigger('user.logout:after', ['user' => $this, 'session' => $session]);
}
}
@@ -515,11 +515,12 @@ class User extends ModelWithContent
*
* @param string $format
* @param string|null $handler
* @param string|null $languageCode
* @return int|string
*/
public function modified(string $format = 'U', string $handler = null)
public function modified(string $format = 'U', string $handler = null, string $languageCode = null)
{
$modifiedContent = F::modified($this->contentFile());
$modifiedContent = F::modified($this->contentFile($languageCode));
$modifiedIndex = F::modified($this->root() . '/index.php');
$modifiedTotal = max([$modifiedContent, $modifiedIndex]);
$handler = $handler ?? $this->kirby()->option('date.handler', 'date');
@@ -682,6 +683,41 @@ class User extends ModelWithContent
return $this->role = Role::nobody();
}
/**
* Returns all available roles
* for this user, that can be selected
* by the authenticated user
*
* @return \Kirby\Cms\Roles
*/
public function roles()
{
$kirby = $this->kirby();
$roles = $kirby->roles();
// a collection with just the one role of the user
$myRole = $roles->filterBy('id', $this->role()->id());
// if there's an authenticated user …
if ($user = $kirby->user()) {
// admin users can select pretty much any role
if ($user->isAdmin() === true) {
// except if the user is the last admin
if ($this->isLastAdmin() === true) {
// in which case they have to stay admin
return $myRole;
}
// return all roles for mighty admins
return $roles;
}
}
// any other user can only keep their role
return $myRole;
}
/**
* The absolute path to the user directory
*
@@ -844,15 +880,17 @@ class User extends ModelWithContent
* String template builder
*
* @param string|null $template
* @param array|null $data
* @param string $fallback Fallback for tokens in the template that cannot be replaced
* @return string
*/
public function toString(string $template = null): string
public function toString(string $template = null, array $data = [], string $fallback = ''): string
{
if ($template === null) {
$template = $this->email();
}
return parent::toString($template);
return parent::toString($template, $data);
}
/**

View File

@@ -29,7 +29,7 @@ trait UserActions
*/
public function changeEmail(string $email)
{
return $this->commit('changeEmail', [$this, $email], function ($user, $email) {
return $this->commit('changeEmail', ['user' => $this, 'email' => $email], function ($user, $email) {
$user = $user->clone([
'email' => $email
]);
@@ -50,7 +50,7 @@ trait UserActions
*/
public function changeLanguage(string $language)
{
return $this->commit('changeLanguage', [$this, $language], function ($user, $language) {
return $this->commit('changeLanguage', ['user' => $this, 'language' => $language], function ($user, $language) {
$user = $user->clone([
'language' => $language,
]);
@@ -71,7 +71,7 @@ trait UserActions
*/
public function changeName(string $name)
{
return $this->commit('changeName', [$this, $name], function ($user, $name) {
return $this->commit('changeName', ['user' => $this, 'name' => $name], function ($user, $name) {
$user = $user->clone([
'name' => $name
]);
@@ -92,7 +92,7 @@ trait UserActions
*/
public function changePassword(string $password)
{
return $this->commit('changePassword', [$this, $password], function ($user, $password) {
return $this->commit('changePassword', ['user' => $this, 'password' => $password], function ($user, $password) {
$user = $user->clone([
'password' => $password = User::hashPassword($password)
]);
@@ -111,7 +111,7 @@ trait UserActions
*/
public function changeRole(string $role)
{
return $this->commit('changeRole', [$this, $role], function ($user, $role) {
return $this->commit('changeRole', ['user' => $this, 'role' => $role], function ($user, $role) {
$user = $user->clone([
'role' => $role,
]);
@@ -144,13 +144,25 @@ trait UserActions
throw new PermissionException('The Kirby user cannot be changed');
}
$old = $this->hardcopy();
$old = $this->hardcopy();
$kirby = $this->kirby();
$argumentValues = array_values($arguments);
$this->rules()->$action(...$arguments);
$this->kirby()->trigger('user.' . $action . ':before', ...$arguments);
$result = $callback(...$arguments);
$this->kirby()->trigger('user.' . $action . ':after', $result, $old);
$this->kirby()->cache('pages')->flush();
$this->rules()->$action(...$argumentValues);
$kirby->trigger('user.' . $action . ':before', $arguments);
$result = $callback(...$argumentValues);
if ($action === 'create') {
$argumentsAfter = ['user' => $result];
} elseif ($action === 'delete') {
$argumentsAfter = ['status' => $result, 'user' => $old];
} else {
$argumentsAfter = ['newUser' => $result, 'oldUser' => $old];
}
$kirby->trigger('user.' . $action . ':after', $argumentsAfter);
$kirby->cache('pages')->flush();
return $result;
}
@@ -181,7 +193,7 @@ trait UserActions
$user = $user->clone(['content' => $form->strings(true)]);
// run the hook
return $user->commit('create', [$user, $props], function ($user, $props) {
return $user->commit('create', ['user' => $user, 'input' => $props], function ($user, $props) {
$user->writeCredentials([
'email' => $user->email(),
'language' => $user->language(),
@@ -231,7 +243,7 @@ trait UserActions
*/
public function delete(): bool
{
return $this->commit('delete', [$this], function ($user) {
return $this->commit('delete', ['user' => $this], function ($user) {
if ($user->exists() === false) {
return true;
}
@@ -258,8 +270,10 @@ trait UserActions
*/
protected function readCredentials(): array
{
if (file_exists($this->root() . '/index.php') === true) {
$credentials = require $this->root() . '/index.php';
$path = $this->root() . '/index.php';
if (is_file($path) === true) {
$credentials = F::load($path);
return is_array($credentials) === false ? [] : $credentials;
} else {

View File

@@ -25,17 +25,7 @@ class UserPermissions extends ModelPermissions
protected function canChangeRole(): bool
{
// only one role, makes no sense to change it
if ($this->user->kirby()->roles()->count() < 2) {
return false;
}
// users who are not admins cannot change their own role
if ($this->user->is($this->model) === true && $this->user->isAdmin() === false) {
return false;
}
return $this->model->isLastAdmin() !== true;
return $this->model->roles()->count() > 1;
}
protected function canCreate(): bool

View File

@@ -3,6 +3,7 @@
namespace Kirby\Cms;
use Kirby\Toolkit\Dir;
use Kirby\Toolkit\F;
use Kirby\Toolkit\Str;
/**
@@ -109,8 +110,9 @@ class Users extends Collection
}
// get role information
if (file_exists($root . '/' . $userDirectory . '/index.php') === true) {
$credentials = require $root . '/' . $userDirectory . '/index.php';
$path = $root . '/' . $userDirectory . '/index.php';
if (is_file($path) === true) {
$credentials = F::load($path);
}
// create user model based on role