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

@@ -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.');
}
}
/**