3.4.0
This commit is contained in:
@@ -293,6 +293,24 @@ class Api
|
||||
return isset($this->data[$key]) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches an object with an array item
|
||||
* based on the `type` field
|
||||
*
|
||||
* @param array models or collections
|
||||
* @param mixed $object
|
||||
*
|
||||
* @return string key of match
|
||||
*/
|
||||
protected function match(array $array, $object = null)
|
||||
{
|
||||
foreach ($array as $definition => $model) {
|
||||
if (is_a($object, $model['type']) === true) {
|
||||
return $definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an API model instance by name
|
||||
*
|
||||
@@ -302,8 +320,15 @@ class Api
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If no model for `$name` exists
|
||||
*/
|
||||
public function model(string $name, $object = null)
|
||||
public function model(string $name = null, $object = null)
|
||||
{
|
||||
// Try to auto-match object with API models
|
||||
if ($name === null) {
|
||||
if ($model = $this->match($this->models, $object)) {
|
||||
$name = $model;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->models[$name]) === false) {
|
||||
throw new NotFoundException(sprintf('The model "%s" does not exist', $name));
|
||||
}
|
||||
@@ -420,29 +445,15 @@ class Api
|
||||
return $object;
|
||||
}
|
||||
|
||||
$className = strtolower(get_class($object));
|
||||
$lastDash = strrpos($className, '\\');
|
||||
|
||||
if ($lastDash !== false) {
|
||||
$className = substr($className, $lastDash + 1);
|
||||
if ($model = $this->match($this->models, $object)) {
|
||||
return $this->model($model, $object);
|
||||
}
|
||||
|
||||
if (isset($this->models[$className]) === true) {
|
||||
return $this->model($className, $object);
|
||||
if ($collection = $this->match($this->collections, $object)) {
|
||||
return $this->collection($collection, $object);
|
||||
}
|
||||
|
||||
if (isset($this->collections[$className]) === true) {
|
||||
return $this->collection($className, $object);
|
||||
}
|
||||
|
||||
// now models deeply by checking for the actual type
|
||||
foreach ($this->models as $modelClass => $model) {
|
||||
if (is_a($object, $model['type']) === true) {
|
||||
return $this->model($modelClass, $object);
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundException(sprintf('The object "%s" cannot be resolved', $className));
|
||||
throw new NotFoundException(sprintf('The object "%s" cannot be resolved', get_class($object)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -29,7 +29,7 @@ class Collection
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->data = $data;
|
||||
$this->model = $schema['model'];
|
||||
$this->model = $schema['model'] ?? null;
|
||||
$this->view = $schema['view'] ?? null;
|
||||
|
||||
if ($data === null) {
|
||||
@@ -40,7 +40,10 @@ class Collection
|
||||
$this->data = $schema['default']->call($this->api);
|
||||
}
|
||||
|
||||
if (isset($schema['type']) === true && is_a($this->data, $schema['type']) === false) {
|
||||
if (
|
||||
isset($schema['type']) === true &&
|
||||
is_a($this->data, $schema['type']) === false
|
||||
) {
|
||||
throw new Exception('Invalid collection type');
|
||||
}
|
||||
}
|
||||
|
@@ -48,7 +48,10 @@ class Model
|
||||
$this->data = $schema['default']->call($this->api);
|
||||
}
|
||||
|
||||
if (isset($schema['type']) === true && is_a($this->data, $schema['type']) === false) {
|
||||
if (
|
||||
isset($schema['type']) === true &&
|
||||
is_a($this->data, $schema['type']) === false
|
||||
) {
|
||||
throw new Exception(sprintf('Invalid model type "%s" expected: "%s"', get_class($this->data), $schema['type']));
|
||||
}
|
||||
}
|
||||
@@ -130,7 +133,10 @@ class Model
|
||||
$value = $this->api->resolve($value);
|
||||
}
|
||||
|
||||
if (is_a($value, 'Kirby\Api\Collection') === true || is_a($value, 'Kirby\Api\Model') === true) {
|
||||
if (
|
||||
is_a($value, 'Kirby\Api\Collection') === true ||
|
||||
is_a($value, 'Kirby\Api\Model') === true
|
||||
) {
|
||||
$selection = $select[$key];
|
||||
|
||||
if ($subview = $selection['view']) {
|
||||
|
@@ -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
|
||||
|
@@ -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.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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
288
kirby/src/Cms/Event.php
Executable 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;
|
||||
}
|
||||
}
|
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ class FileBlueprint extends Blueprint
|
||||
'changeName' => null,
|
||||
'create' => null,
|
||||
'delete' => null,
|
||||
'read' => null,
|
||||
'replace' => null,
|
||||
'update' => null,
|
||||
]
|
||||
|
@@ -298,6 +298,6 @@ class Filename
|
||||
'name' => $this->name(),
|
||||
'attributes' => $this->attributesToString('-'),
|
||||
'extension' => $this->extension()
|
||||
]);
|
||||
], '');
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
*/
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* The `Data` class provides readers and
|
||||
* writers for data. The class comes with
|
||||
* four handlers for `json`, `php`, `txt`
|
||||
* handlers for `json`, `php`, `txt`, `xml`
|
||||
* and `yaml` encoded data, but can be
|
||||
* extended and customized.
|
||||
*
|
||||
@@ -32,6 +32,7 @@ class Data
|
||||
public static $aliases = [
|
||||
'md' => 'txt',
|
||||
'mdown' => 'txt',
|
||||
'rss' => 'xml',
|
||||
'yml' => 'yaml',
|
||||
];
|
||||
|
||||
@@ -44,6 +45,7 @@ class Data
|
||||
'json' => 'Kirby\Data\Json',
|
||||
'php' => 'Kirby\Data\PHP',
|
||||
'txt' => 'Kirby\Data\Txt',
|
||||
'xml' => 'Kirby\Data\Xml',
|
||||
'yaml' => 'Kirby\Data\Yaml',
|
||||
];
|
||||
|
||||
@@ -73,23 +75,23 @@ class Data
|
||||
/**
|
||||
* Decodes data with the specified handler
|
||||
*
|
||||
* @param string $data
|
||||
* @param mixed $string
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function decode(string $data = null, string $type): array
|
||||
public static function decode($string = null, string $type): array
|
||||
{
|
||||
return static::handler($type)->decode($data);
|
||||
return static::handler($type)->decode($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes data with the specified handler
|
||||
*
|
||||
* @param array $data
|
||||
* @param mixed $data
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(array $data = null, string $type): string
|
||||
public static function encode($data = null, string $type): string
|
||||
{
|
||||
return static::handler($type)->encode($data);
|
||||
}
|
||||
@@ -114,11 +116,11 @@ class Data
|
||||
* the extension if not specified
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $data
|
||||
* @param mixed $data
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
public static function write(string $file = null, array $data = [], string $type = null): bool
|
||||
public static function write(string $file = null, $data = [], string $type = null): bool
|
||||
{
|
||||
return static::handler($type ?? F::extension($file))->write($file, $data);
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
@@ -23,7 +23,7 @@ abstract class Handler
|
||||
*
|
||||
* Needs to throw an Exception if the file can't be parsed.
|
||||
*
|
||||
* @param string $string
|
||||
* @param mixed $string
|
||||
* @return array
|
||||
*/
|
||||
abstract public static function decode($string): array;
|
||||
@@ -55,10 +55,10 @@ abstract class Handler
|
||||
* Writes data to a file
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $data
|
||||
* @param mixed $data
|
||||
* @return bool
|
||||
*/
|
||||
public static function write(string $file = null, array $data = []): bool
|
||||
public static function write(string $file = null, $data = []): bool
|
||||
{
|
||||
return F::write($file, static::encode($data));
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Simple Wrapper around json_encode and json_decode
|
||||
@@ -29,17 +29,29 @@ class Json extends Handler
|
||||
/**
|
||||
* Parses an encoded JSON string and returns a multi-dimensional array
|
||||
*
|
||||
* @param string $json
|
||||
* @param mixed $string
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($json): array
|
||||
public static function decode($string): array
|
||||
{
|
||||
$result = json_decode($json, true);
|
||||
if ($string === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($string) === true) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
if (is_string($string) === false) {
|
||||
throw new InvalidArgumentException('Invalid JSON data; please pass a string');
|
||||
}
|
||||
|
||||
$result = json_decode($string, true);
|
||||
|
||||
if (is_array($result) === true) {
|
||||
return $result;
|
||||
} else {
|
||||
throw new Exception('JSON string is invalid');
|
||||
throw new InvalidArgumentException('JSON string is invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,7 @@ class PHP extends Handler
|
||||
* @param string $indent For internal use only
|
||||
* @return string
|
||||
*/
|
||||
public static function encode($data, $indent = ''): string
|
||||
public static function encode($data, string $indent = ''): string
|
||||
{
|
||||
switch (gettype($data)) {
|
||||
case 'array':
|
||||
@@ -46,14 +47,14 @@ class PHP extends Handler
|
||||
}
|
||||
|
||||
/**
|
||||
* PHP arrays don't have to be decoded
|
||||
* PHP strings shouldn't be decoded manually
|
||||
*
|
||||
* @param array $array
|
||||
* @param mixed $array
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($array): array
|
||||
{
|
||||
return $array;
|
||||
throw new BadMethodCallException('The PHP::decode() method is not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,17 +69,17 @@ class PHP extends Handler
|
||||
throw new Exception('The file "' . $file . '" does not exist');
|
||||
}
|
||||
|
||||
return (array)(include $file);
|
||||
return (array)F::load($file, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PHP file with the given data
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $data
|
||||
* @param mixed $data
|
||||
* @return bool
|
||||
*/
|
||||
public static function write(string $file = null, array $data = []): bool
|
||||
public static function write(string $file = null, $data = []): bool
|
||||
{
|
||||
$php = static::encode($data);
|
||||
$php = "<?php\n\nreturn $php;";
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
@@ -25,7 +27,7 @@ class Txt extends Handler
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ((array)$data as $key => $value) {
|
||||
foreach (A::wrap($data) as $key => $value) {
|
||||
if (empty($key) === true || $value === null) {
|
||||
continue;
|
||||
}
|
||||
@@ -48,7 +50,7 @@ class Txt extends Handler
|
||||
{
|
||||
// avoid problems with arrays
|
||||
if (is_array($value) === true) {
|
||||
$value = Yaml::encode($value);
|
||||
$value = Data::encode($value, 'yaml');
|
||||
// avoid problems with localized floats
|
||||
} elseif (is_float($value) === true) {
|
||||
$value = Str::float($value);
|
||||
@@ -86,11 +88,23 @@ class Txt extends Handler
|
||||
/**
|
||||
* Parses a Kirby txt string and returns a multi-dimensional array
|
||||
*
|
||||
* @param string $string
|
||||
* @param mixed $string
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($string): array
|
||||
{
|
||||
if ($string === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($string) === true) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
if (is_string($string) === false) {
|
||||
throw new InvalidArgumentException('Invalid TXT data; please pass a string');
|
||||
}
|
||||
|
||||
// remove BOM
|
||||
$string = str_replace("\xEF\xBB\xBF", '', $string);
|
||||
// explode all fields by the line separator
|
||||
|
64
kirby/src/Data/Xml.php
Executable file
64
kirby/src/Data/Xml.php
Executable file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Xml as XmlConverter;
|
||||
|
||||
/**
|
||||
* Simple Wrapper around the XML parser of the Toolkit
|
||||
*
|
||||
* @package Kirby Data
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Xml extends Handler
|
||||
{
|
||||
/**
|
||||
* Converts an array to an encoded XML string
|
||||
*
|
||||
* @param mixed $data
|
||||
* @return string
|
||||
*/
|
||||
public static function encode($data): string
|
||||
{
|
||||
return XmlConverter::create($data, 'data');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an encoded XML string and returns a multi-dimensional array
|
||||
*
|
||||
* @param mixed $string
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($string): array
|
||||
{
|
||||
if ($string === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($string) === true) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
if (is_string($string) === false) {
|
||||
throw new InvalidArgumentException('Invalid XML data; please pass a string');
|
||||
}
|
||||
|
||||
$result = XmlConverter::parse($string);
|
||||
|
||||
if (is_array($result) === true) {
|
||||
// remove the root's name if it is the default <data> to ensure that
|
||||
// the decoded data is the same as the input to the encode() method
|
||||
if ($result['@name'] === 'data') {
|
||||
unset($result['@name']);
|
||||
}
|
||||
|
||||
return $result;
|
||||
} else {
|
||||
throw new InvalidArgumentException('XML string is invalid');
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Spyc;
|
||||
|
||||
/**
|
||||
@@ -42,29 +42,33 @@ class Yaml extends Handler
|
||||
/**
|
||||
* Parses an encoded YAML string and returns a multi-dimensional array
|
||||
*
|
||||
* @param string $yaml
|
||||
* @param mixed $string
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($yaml): array
|
||||
public static function decode($string): array
|
||||
{
|
||||
if ($yaml === null) {
|
||||
if ($string === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($yaml) === true) {
|
||||
return $yaml;
|
||||
if (is_array($string) === true) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
if (is_string($string) === false) {
|
||||
throw new InvalidArgumentException('Invalid YAML data; please pass a string');
|
||||
}
|
||||
|
||||
// remove BOM
|
||||
$yaml = str_replace("\xEF\xBB\xBF", '', $yaml);
|
||||
$result = Spyc::YAMLLoadString($yaml);
|
||||
$string = str_replace("\xEF\xBB\xBF", '', $string);
|
||||
$result = Spyc::YAMLLoadString($string);
|
||||
|
||||
if (is_array($result)) {
|
||||
return $result;
|
||||
} else {
|
||||
// apparently Spyc always returns an array, even for invalid YAML syntax
|
||||
// so this Exception should currently never be thrown
|
||||
throw new Exception('YAML string is invalid'); // @codeCoverageIgnore
|
||||
throw new InvalidArgumentException('The YAML data cannot be parsed'); // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Kirby\Database;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
@@ -230,7 +231,7 @@ class Database
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the exception mode for the next query
|
||||
* Sets the exception mode
|
||||
*
|
||||
* @param bool $fail
|
||||
* @return \Kirby\Database\Database
|
||||
@@ -369,7 +370,7 @@ class Database
|
||||
$this->statement->execute($bindings);
|
||||
|
||||
$this->affected = $this->statement->rowCount();
|
||||
$this->lastId = $this->connection->lastInsertId();
|
||||
$this->lastId = Str::startsWith($query, 'insert ', true) ? $this->connection->lastInsertId() : null;
|
||||
$this->lastError = null;
|
||||
|
||||
// store the final sql to add it to the trace later
|
||||
@@ -395,9 +396,6 @@ class Database
|
||||
'error' => $this->lastError
|
||||
]);
|
||||
|
||||
// reset some stuff
|
||||
$this->fail = false;
|
||||
|
||||
// return true or false on success or failure
|
||||
return $this->lastError === null;
|
||||
}
|
||||
@@ -426,7 +424,11 @@ class Database
|
||||
}
|
||||
|
||||
// define the default flag for the fetch method
|
||||
$flags = $options['fetch'] === 'array' ? PDO::FETCH_ASSOC : PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE;
|
||||
if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') {
|
||||
$flags = PDO::FETCH_ASSOC;
|
||||
} else {
|
||||
$flags = PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE;
|
||||
}
|
||||
|
||||
// add optional flags
|
||||
if (empty($options['flag']) === false) {
|
||||
@@ -434,7 +436,7 @@ class Database
|
||||
}
|
||||
|
||||
// set the fetch mode
|
||||
if ($options['fetch'] === 'array') {
|
||||
if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') {
|
||||
$this->statement->setFetchMode($flags);
|
||||
} else {
|
||||
$this->statement->setFetchMode($flags, $options['fetch']);
|
||||
@@ -443,6 +445,13 @@ class Database
|
||||
// fetch that stuff
|
||||
$results = $this->statement->{$options['method']}();
|
||||
|
||||
// apply the fetch closure to all results if given
|
||||
if ($options['fetch'] instanceof Closure) {
|
||||
foreach ($results as $key => $result) {
|
||||
$results[$key] = $options['fetch']($result, $key);
|
||||
}
|
||||
}
|
||||
|
||||
if ($options['iterator'] === 'array') {
|
||||
return $this->lastResult = $results;
|
||||
}
|
||||
@@ -559,6 +568,11 @@ class Database
|
||||
}
|
||||
}
|
||||
|
||||
// update cache
|
||||
if (in_array($table, $this->tableWhitelist) !== true) {
|
||||
$this->tableWhitelist[] = $table;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -571,7 +585,17 @@ class Database
|
||||
public function dropTable($table): bool
|
||||
{
|
||||
$sql = $this->sql()->dropTable($table);
|
||||
return $this->execute($sql['query'], $sql['bindings']);
|
||||
if ($this->execute($sql['query'], $sql['bindings']) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// update cache
|
||||
$key = array_search($this->tableWhitelist, $table);
|
||||
if ($key !== false) {
|
||||
unset($this->tableWhitelist[$key]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Kirby\Database;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Config;
|
||||
|
||||
/**
|
||||
@@ -16,8 +16,6 @@ use Kirby\Toolkit\Config;
|
||||
*/
|
||||
class Db
|
||||
{
|
||||
const ERROR_UNKNOWN_METHOD = 0;
|
||||
|
||||
/**
|
||||
* Query shortcuts
|
||||
*
|
||||
@@ -35,10 +33,11 @@ class Db
|
||||
/**
|
||||
* (Re)connect the database
|
||||
*
|
||||
* @param array $params Pass [] to use the default params from the config
|
||||
* @param array|null $params Pass `[]` to use the default params from the config,
|
||||
* don't pass any argument to get the current connection
|
||||
* @return \Kirby\Database\Database
|
||||
*/
|
||||
public static function connect(array $params = null)
|
||||
public static function connect(?array $params = null)
|
||||
{
|
||||
if ($params === null && static::$connection !== null) {
|
||||
return static::$connection;
|
||||
@@ -46,14 +45,15 @@ class Db
|
||||
|
||||
// try to connect with the default
|
||||
// connection settings if no params are set
|
||||
$params = $params ?? [
|
||||
$defaults = [
|
||||
'type' => Config::get('db.type', 'mysql'),
|
||||
'host' => Config::get('db.host', 'localhost'),
|
||||
'user' => Config::get('db.user', 'root'),
|
||||
'password' => Config::get('db.password', ''),
|
||||
'database' => Config::get('db.database', ''),
|
||||
'prefix' => Config::get('db.prefix', ''),
|
||||
'prefix' => Config::get('db.prefix', '')
|
||||
];
|
||||
$params = $params ?? $defaults;
|
||||
|
||||
return static::$connection = new Database($params);
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class Db
|
||||
/**
|
||||
* Returns the current database connection
|
||||
*
|
||||
* @return \Kirby\Database\Database
|
||||
* @return \Kirby\Database\Database|null
|
||||
*/
|
||||
public static function connection()
|
||||
{
|
||||
@@ -69,7 +69,7 @@ class Db
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current table, which should be queried. Returns a
|
||||
* Sets the current table which should be queried. Returns a
|
||||
* Query object, which can be used to build a full query for
|
||||
* that table.
|
||||
*
|
||||
@@ -83,7 +83,7 @@ class Db
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a raw sql query which expects a set of results
|
||||
* Executes a raw SQL query which expects a set of results
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $bindings
|
||||
@@ -97,21 +97,22 @@ class Db
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a raw sql query which expects no set of results (i.e. update, insert, delete)
|
||||
* Executes a raw SQL query which expects no set of results (i.e. update, insert, delete)
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $bindings
|
||||
* @return mixed
|
||||
* @return bool
|
||||
*/
|
||||
public static function execute(string $query, array $bindings = [])
|
||||
public static function execute(string $query, array $bindings = []): bool
|
||||
{
|
||||
$db = static::connect();
|
||||
return $db->execute($query, $bindings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic calls for other static db methods,
|
||||
* which are redircted to the database class if available
|
||||
* Magic calls for other static Db methods are
|
||||
* redirected to either a predefined query or
|
||||
* the respective method of the Database object
|
||||
*
|
||||
* @param string $method
|
||||
* @param mixed $arguments
|
||||
@@ -123,20 +124,21 @@ class Db
|
||||
return static::$queries[$method](...$arguments);
|
||||
}
|
||||
|
||||
if (is_callable([static::$connection, $method]) === true) {
|
||||
if (static::$connection !== null && method_exists(static::$connection, $method) === true) {
|
||||
return call_user_func_array([static::$connection, $method], $arguments);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Invalid static Db method: ' . $method, static::ERROR_UNKNOWN_METHOD);
|
||||
throw new InvalidArgumentException('Invalid static Db method: ' . $method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for select clauses
|
||||
* Shortcut for SELECT clauses
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param mixed $columns Either a string with columns or an array of column names
|
||||
* @param mixed $where The where clause. Can be a string or an array
|
||||
* @param mixed $where The WHERE clause; can be a string or an array
|
||||
* @param string $order
|
||||
* @param int $offset
|
||||
* @param int $limit
|
||||
@@ -148,10 +150,11 @@ Db::$queries['select'] = function (string $table, $columns = '*', $where = null,
|
||||
|
||||
/**
|
||||
* Shortcut for selecting a single row in a table
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param mixed $columns Either a string with columns or an array of column names
|
||||
* @param mixed $where The where clause. Can be a string or an array
|
||||
* @param mixed $where The WHERE clause; can be a string or an array
|
||||
* @param string $order
|
||||
* @param int $offset
|
||||
* @param int $limit
|
||||
@@ -163,10 +166,11 @@ Db::$queries['first'] = Db::$queries['row'] = Db::$queries['one'] = function (st
|
||||
|
||||
/**
|
||||
* Returns only values from a single column
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param string $column The name of the column to select from
|
||||
* @param mixed $where The where clause. Can be a string or an array
|
||||
* @param mixed $where The WHERE clause; can be a string or an array
|
||||
* @param string $order
|
||||
* @param int $offset
|
||||
* @param int $limit
|
||||
@@ -178,93 +182,101 @@ Db::$queries['column'] = function (string $table, string $column, $where = null,
|
||||
|
||||
/**
|
||||
* Shortcut for inserting a new row into a table
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param array $values An array of values, which should be inserted
|
||||
* @return bool
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param array $values An array of values which should be inserted
|
||||
* @return int ID of the inserted row
|
||||
*/
|
||||
Db::$queries['insert'] = function (string $table, array $values) {
|
||||
Db::$queries['insert'] = function (string $table, array $values): int {
|
||||
return Db::table($table)->insert($values);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for updating a row in a table
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param array $values An array of values, which should be inserted
|
||||
* @param mixed $where An optional where clause
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param array $values An array of values which should be inserted
|
||||
* @param mixed $where An optional WHERE clause
|
||||
* @return bool
|
||||
*/
|
||||
Db::$queries['update'] = function (string $table, array $values, $where = null) {
|
||||
Db::$queries['update'] = function (string $table, array $values, $where = null): bool {
|
||||
return Db::table($table)->where($where)->update($values);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for deleting rows in a table
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param mixed $where An optional where clause
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param mixed $where An optional WHERE clause
|
||||
* @return bool
|
||||
*/
|
||||
Db::$queries['delete'] = function (string $table, $where = null) {
|
||||
Db::$queries['delete'] = function (string $table, $where = null): bool {
|
||||
return Db::table($table)->where($where)->delete();
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for counting rows in a table
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param mixed $where An optional where clause
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param mixed $where An optional WHERE clause
|
||||
* @return int
|
||||
*/
|
||||
Db::$queries['count'] = function (string $table, $where = null) {
|
||||
Db::$queries['count'] = function (string $table, $where = null): int {
|
||||
return Db::table($table)->where($where)->count();
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for calculating the minimum value in a column
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param string $column The name of the column of which the minimum should be calculated
|
||||
* @param mixed $where An optional where clause
|
||||
* @return mixed
|
||||
* @param mixed $where An optional WHERE clause
|
||||
* @return float
|
||||
*/
|
||||
Db::$queries['min'] = function (string $table, string $column, $where = null) {
|
||||
Db::$queries['min'] = function (string $table, string $column, $where = null): float {
|
||||
return Db::table($table)->where($where)->min($column);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for calculating the maximum value in a column
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param string $column The name of the column of which the maximum should be calculated
|
||||
* @param mixed $where An optional where clause
|
||||
* @return mixed
|
||||
* @param mixed $where An optional WHERE clause
|
||||
* @return float
|
||||
*/
|
||||
Db::$queries['max'] = function (string $table, string $column, $where = null) {
|
||||
Db::$queries['max'] = function (string $table, string $column, $where = null): float {
|
||||
return Db::table($table)->where($where)->max($column);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for calculating the average value in a column
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param string $column The name of the column of which the average should be calculated
|
||||
* @param mixed $where An optional where clause
|
||||
* @return mixed
|
||||
* @param mixed $where An optional WHERE clause
|
||||
* @return float
|
||||
*/
|
||||
Db::$queries['avg'] = function (string $table, string $column, $where = null) {
|
||||
Db::$queries['avg'] = function (string $table, string $column, $where = null): float {
|
||||
return Db::table($table)->where($where)->avg($column);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for calculating the sum of all values in a column
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $table The name of the table which should be queried
|
||||
* @param string $column The name of the column of which the sum should be calculated
|
||||
* @param mixed $where An optional where clause
|
||||
* @return mixed
|
||||
* @param mixed $where An optional WHERE clause
|
||||
* @return float
|
||||
*/
|
||||
Db::$queries['sum'] = function (string $table, string $column, $where = null) {
|
||||
Db::$queries['sum'] = function (string $table, string $column, $where = null): float {
|
||||
return Db::table($table)->where($where)->sum($column);
|
||||
};
|
||||
|
@@ -30,8 +30,9 @@ class Query
|
||||
|
||||
/**
|
||||
* The object which should be fetched for each row
|
||||
* or function to call for each row
|
||||
*
|
||||
* @var string
|
||||
* @var string|\Closure
|
||||
*/
|
||||
protected $fetch = 'Kirby\Toolkit\Obj';
|
||||
|
||||
@@ -219,13 +220,14 @@ class Query
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the object class, which should be fetched
|
||||
* Set this to array to get a simple array instead of an object
|
||||
* Sets the object class, which should be fetched;
|
||||
* set this to `'array'` to get a simple array instead of an object;
|
||||
* pass a function that receives the `$data` and the `$key` to generate arbitrary data structures
|
||||
*
|
||||
* @param string $fetch
|
||||
* @param string|\Closure $fetch
|
||||
* @return \Kirby\Database\Query
|
||||
*/
|
||||
public function fetch(string $fetch)
|
||||
public function fetch($fetch)
|
||||
{
|
||||
$this->fetch = $fetch;
|
||||
return $this;
|
||||
@@ -559,55 +561,55 @@ class Query
|
||||
/**
|
||||
* Builds a count query
|
||||
*
|
||||
* @return \Kirby\Database\Query
|
||||
* @return int
|
||||
*/
|
||||
public function count()
|
||||
public function count(): int
|
||||
{
|
||||
return $this->aggregate('COUNT');
|
||||
return (int)$this->aggregate('COUNT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a max query
|
||||
*
|
||||
* @param string $column
|
||||
* @return \Kirby\Database\Query
|
||||
* @return float
|
||||
*/
|
||||
public function max(string $column)
|
||||
public function max(string $column): float
|
||||
{
|
||||
return $this->aggregate('MAX', $column);
|
||||
return (float)$this->aggregate('MAX', $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a min query
|
||||
*
|
||||
* @param string $column
|
||||
* @return \Kirby\Database\Query
|
||||
* @return float
|
||||
*/
|
||||
public function min(string $column)
|
||||
public function min(string $column): float
|
||||
{
|
||||
return $this->aggregate('MIN', $column);
|
||||
return (float)$this->aggregate('MIN', $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a sum query
|
||||
*
|
||||
* @param string $column
|
||||
* @return \Kirby\Database\Query
|
||||
* @return float
|
||||
*/
|
||||
public function sum(string $column)
|
||||
public function sum(string $column): float
|
||||
{
|
||||
return $this->aggregate('SUM', $column);
|
||||
return (float)$this->aggregate('SUM', $column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an average query
|
||||
*
|
||||
* @param string $column
|
||||
* @return \Kirby\Database\Query
|
||||
* @return float
|
||||
*/
|
||||
public function avg(string $column)
|
||||
public function avg(string $column): float
|
||||
{
|
||||
return $this->aggregate('AVG', $column);
|
||||
return (float)$this->aggregate('AVG', $column);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -812,10 +814,16 @@ class Query
|
||||
*/
|
||||
public function column($column)
|
||||
{
|
||||
$sql = $this->database->sql();
|
||||
$primaryKey = $sql->combineIdentifier($this->table, $this->primaryKeyName);
|
||||
// if there isn't already an explicit order, order by the primary key
|
||||
// instead of the column that was requested (which would be implied otherwise)
|
||||
if ($this->order === null) {
|
||||
$sql = $this->database->sql();
|
||||
$primaryKey = $sql->combineIdentifier($this->table, $this->primaryKeyName);
|
||||
|
||||
$results = $this->query($this->select([$column])->order($primaryKey . ' ASC')->build('select'), [
|
||||
$this->order($primaryKey . ' ASC');
|
||||
}
|
||||
|
||||
$results = $this->query($this->select([$column])->build('select'), [
|
||||
'iterator' => 'array',
|
||||
'fetch' => 'array',
|
||||
]);
|
||||
|
@@ -15,7 +15,7 @@ use Kirby\Toolkit\Str;
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Sql
|
||||
abstract class Sql
|
||||
{
|
||||
/**
|
||||
* List of literals which should not be escaped in queries
|
||||
@@ -27,12 +27,21 @@ class Sql
|
||||
/**
|
||||
* The parent database connection
|
||||
*
|
||||
* @var Database
|
||||
* @var \Kirby\Database\Database
|
||||
*/
|
||||
public $database;
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* List of used bindings; used to avoid
|
||||
* duplicate binding names
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $bindings = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param \Kirby\Database\Database $database
|
||||
*/
|
||||
@@ -44,51 +53,45 @@ class Sql
|
||||
/**
|
||||
* Returns a randomly generated binding name
|
||||
*
|
||||
* @param string $label String that contains lowercase letters and numbers to use as a readable identifier
|
||||
* @param string $prefix
|
||||
* @return string
|
||||
* @param string $label String that only contains alphanumeric chars and
|
||||
* underscores to use as a human-readable identifier
|
||||
* @return string Binding name that is guaranteed to be unique for this connection
|
||||
*/
|
||||
public function bindingName(string $label): string
|
||||
{
|
||||
// make sure that the binding name is valid to prevent injections
|
||||
if (!preg_match('/^[a-z0-9_]+$/', $label)) {
|
||||
// make sure that the binding name is safe to prevent injections;
|
||||
// otherwise use a generic label
|
||||
if (!$label || preg_match('/^[a-zA-Z0-9_]+$/', $label) !== 1) {
|
||||
$label = 'invalid';
|
||||
}
|
||||
|
||||
return ':' . $label . '_' . Str::random(16);
|
||||
// generate random bindings until the name is unique
|
||||
do {
|
||||
$binding = ':' . $label . '_' . Str::random(8, 'alphaNum');
|
||||
} while (in_array($binding, $this->bindings) === true);
|
||||
|
||||
// cache the generated binding name for future invocations
|
||||
$this->bindings[] = $binding;
|
||||
return $binding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of columns for a specified table
|
||||
* MySQL version
|
||||
* Returns a query to list the columns of a specified table;
|
||||
* the query needs to return rows with a column `name`
|
||||
*
|
||||
* @param string $table The table name
|
||||
* @param string $table Table name
|
||||
* @return array
|
||||
*/
|
||||
public function columns(string $table): array
|
||||
{
|
||||
$databaseBinding = $this->bindingName('database');
|
||||
$tableBinding = $this->bindingName('table');
|
||||
|
||||
$query = 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS ';
|
||||
$query .= 'WHERE TABLE_SCHEMA = ' . $databaseBinding . ' AND TABLE_NAME = ' . $tableBinding;
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'bindings' => [
|
||||
$databaseBinding => $this->database->name(),
|
||||
$tableBinding => $table,
|
||||
]
|
||||
];
|
||||
}
|
||||
abstract public function columns(string $table): array;
|
||||
|
||||
/**
|
||||
* Optionl default value definition for the column
|
||||
* Returns a query snippet for a column default value
|
||||
*
|
||||
* @param array $column
|
||||
* @return array
|
||||
* @param string $name Column name
|
||||
* @param array $column Column definition array with an optional `default` key
|
||||
* @return array Array with a `query` string and a `bindings` array
|
||||
*/
|
||||
public function columnDefault(array $column): array
|
||||
public function columnDefault(string $name, array $column): array
|
||||
{
|
||||
if (isset($column['default']) === false) {
|
||||
return [
|
||||
@@ -97,74 +100,63 @@ class Sql
|
||||
];
|
||||
}
|
||||
|
||||
$binding = $this->bindingName($column['name'] . '_default');
|
||||
$binding = $this->bindingName($name . '_default');
|
||||
|
||||
return [
|
||||
'query' => 'DEFAULT ' . $binding,
|
||||
'bindings' => [
|
||||
$binding = $column['default']
|
||||
$binding => $column['default']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid column name
|
||||
* Returns the cleaned identifier based on the table and column name
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $column
|
||||
* @param bool $enforceQualified
|
||||
* @return string|null
|
||||
* @param string $table Table name
|
||||
* @param string $column Column name
|
||||
* @param bool $enforceQualified If true, a qualified identifier is returned in all cases
|
||||
* @return string|null Identifier or null if the table or column is invalid
|
||||
*/
|
||||
public function columnName(string $table, string $column, bool $enforceQualified = false): ?string
|
||||
{
|
||||
// ensure we have clean $table and $column values without qualified identifiers
|
||||
list($table, $column) = $this->splitIdentifier($table, $column);
|
||||
|
||||
if ($this->validateColumn($table, $column) === true) {
|
||||
// combine the identifiers again
|
||||
if ($this->database->validateColumn($table, $column) === true) {
|
||||
return $this->combineIdentifier($table, $column, $enforceQualified !== true);
|
||||
}
|
||||
|
||||
// the table or column does not exist
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracted column types to simplify table
|
||||
* creation for multiple database drivers
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function columnTypes(): array
|
||||
{
|
||||
return [
|
||||
'id' => '{{ name }} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT',
|
||||
'varchar' => '{{ name }} varchar(255) {{ null }} {{ default }}',
|
||||
'text' => '{{ name }} TEXT',
|
||||
'int' => '{{ name }} INT(11) UNSIGNED {{ null }} {{ default }}',
|
||||
'timestamp' => '{{ name }} TIMESTAMP {{ null }} {{ default }}'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional key definition for the column.
|
||||
*
|
||||
* @param array $column
|
||||
* @return array
|
||||
*/
|
||||
public function columnKey(array $column): array
|
||||
{
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => []
|
||||
'id' => '{{ name }} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY',
|
||||
'varchar' => '{{ name }} varchar(255) {{ null }} {{ default }} {{ unique }}',
|
||||
'text' => '{{ name }} TEXT {{ unique }}',
|
||||
'int' => '{{ name }} INT(11) UNSIGNED {{ null }} {{ default }} {{ unique }}',
|
||||
'timestamp' => '{{ name }} TIMESTAMP {{ null }} {{ default }} {{ unique }}'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines an identifier (table and column)
|
||||
* Default version for MySQL
|
||||
*
|
||||
* @param $table string
|
||||
* @param $column string
|
||||
* @param $values boolean Whether the identifier is going to be used for a values clause
|
||||
* Only relevant for SQLite
|
||||
* @param $values boolean Whether the identifier is going to be used for a VALUES clause;
|
||||
* only relevant for SQLite
|
||||
* @return string
|
||||
*/
|
||||
public function combineIdentifier(string $table, string $column, bool $values = false): string
|
||||
@@ -173,33 +165,29 @@ class Sql
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the create syntax for a single column
|
||||
* Creates the CREATE TABLE syntax for a single column
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $column
|
||||
* @return array
|
||||
* @param string $name Column name
|
||||
* @param array $column Column definition array; valid keys:
|
||||
* - `type` (required): Column template to use
|
||||
* - `null`: Whether the column may be NULL (boolean)
|
||||
* - `key`: Index this column is part of; special values `'primary'` for PRIMARY KEY and `true` for automatic naming
|
||||
* - `unique`: Whether the index (or if not set the column itself) has a UNIQUE constraint
|
||||
* - `default`: Default value of this column
|
||||
* @return array Array with `query` and `key` strings, a `unique` boolean and a `bindings` array
|
||||
*/
|
||||
public function createColumn(string $table, array $column): array
|
||||
public function createColumn(string $name, array $column): array
|
||||
{
|
||||
// column type
|
||||
if (isset($column['type']) === false) {
|
||||
throw new InvalidArgumentException('No column type given for column ' . $column);
|
||||
throw new InvalidArgumentException('No column type given for column ' . $name);
|
||||
}
|
||||
|
||||
// column name
|
||||
if (isset($column['name']) === false) {
|
||||
throw new InvalidArgumentException('No column name given');
|
||||
}
|
||||
|
||||
if ($column['type'] === 'id') {
|
||||
$column['key'] = 'PRIMARY';
|
||||
}
|
||||
|
||||
if (!$template = ($this->columnTypes()[$column['type']] ?? null)) {
|
||||
$template = $this->columnTypes()[$column['type']] ?? null;
|
||||
if (!$template) {
|
||||
throw new InvalidArgumentException('Unsupported column type: ' . $column['type']);
|
||||
}
|
||||
|
||||
// null
|
||||
// null option
|
||||
if (A::get($column, 'null') === false) {
|
||||
$null = 'NOT NULL';
|
||||
} else {
|
||||
@@ -207,85 +195,124 @@ class Sql
|
||||
}
|
||||
|
||||
// indexes/keys
|
||||
$key = false;
|
||||
|
||||
if (isset($column['key']) === true) {
|
||||
$column['key'] = strtoupper($column['key']);
|
||||
|
||||
// backwards compatibility
|
||||
if ($column['key'] === 'PRIMARY') {
|
||||
$column['key'] = 'PRIMARY KEY';
|
||||
if (is_string($column['key']) === true) {
|
||||
$column['key'] = strtolower($column['key']);
|
||||
} elseif ($column['key'] === true) {
|
||||
$column['key'] = $name . '_index';
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($column['key'], ['PRIMARY KEY', 'INDEX']) === true) {
|
||||
$key = $column['key'];
|
||||
// unique
|
||||
$uniqueKey = false;
|
||||
$uniqueColumn = null;
|
||||
if (isset($column['unique']) === true && $column['unique'] === true) {
|
||||
if (isset($column['key']) === true) {
|
||||
// this column is part of an index, make that unique
|
||||
$uniqueKey = true;
|
||||
} else {
|
||||
// make the column itself unique
|
||||
$uniqueColumn = 'UNIQUE';
|
||||
}
|
||||
}
|
||||
|
||||
// default value
|
||||
$columnDefault = $this->columnDefault($column);
|
||||
$columnKey = $this->columnKey($column);
|
||||
$columnDefault = $this->columnDefault($name, $column);
|
||||
|
||||
$query = trim(Str::template($template, [
|
||||
'name' => $this->quoteIdentifier($column['name']),
|
||||
'name' => $this->quoteIdentifier($name),
|
||||
'null' => $null,
|
||||
'key' => $columnKey['query'],
|
||||
'default' => $columnDefault['query'],
|
||||
]));
|
||||
|
||||
$bindings = array_merge($columnKey['bindings'], $columnDefault['bindings']);
|
||||
'unique' => $uniqueColumn
|
||||
], ''));
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'bindings' => $bindings,
|
||||
'key' => $key
|
||||
'bindings' => $columnDefault['bindings'],
|
||||
'key' => $column['key'] ?? null,
|
||||
'unique' => $uniqueKey
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table with a simple scheme array for columns
|
||||
* Default version for MySQL
|
||||
* Creates the inner query for the columns in a CREATE TABLE query
|
||||
*
|
||||
* @param string $table The table name
|
||||
* @param array $columns
|
||||
* @return array
|
||||
* @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()`
|
||||
* @return array Array with a `query` string and `bindings`, `keys` and `unique` arrays
|
||||
*/
|
||||
public function createTable(string $table, array $columns = []): array
|
||||
public function createTableInner(array $columns): array
|
||||
{
|
||||
$output = [];
|
||||
$keys = [];
|
||||
$query = [];
|
||||
$bindings = [];
|
||||
$keys = [];
|
||||
$unique = [];
|
||||
|
||||
foreach ($columns as $name => $column) {
|
||||
$sql = $this->createColumn($table, $column);
|
||||
$sql = $this->createColumn($name, $column);
|
||||
|
||||
$output[] = $sql['query'];
|
||||
// collect query and bindings
|
||||
$query[] = $sql['query'];
|
||||
$bindings += $sql['bindings'];
|
||||
|
||||
if ($sql['key']) {
|
||||
$keys[$column['name']] = $sql['key'];
|
||||
// make a list of keys per key name
|
||||
if ($sql['key'] !== null) {
|
||||
if (isset($keys[$sql['key']]) !== true) {
|
||||
$keys[$sql['key']] = [];
|
||||
}
|
||||
|
||||
$keys[$sql['key']][] = $name;
|
||||
if ($sql['unique'] === true) {
|
||||
$unique[$sql['key']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$bindings = array_merge($bindings, $sql['bindings']);
|
||||
}
|
||||
|
||||
// combine columns
|
||||
$inner = implode(',' . PHP_EOL, $output);
|
||||
|
||||
// add keys
|
||||
foreach ($keys as $name => $key) {
|
||||
$inner .= ',' . PHP_EOL . $key . ' (' . $this->quoteIdentifier($name) . ')';
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner . PHP_EOL . ')',
|
||||
'bindings' => $bindings
|
||||
'query' => implode(',' . PHP_EOL, $query),
|
||||
'bindings' => $bindings,
|
||||
'keys' => $keys,
|
||||
'unique' => $unique
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a delete clause
|
||||
* Creates a CREATE TABLE query
|
||||
*
|
||||
* @param array $params List of parameters for the delete clause. See defaults for more info.
|
||||
* @param string $table Table name
|
||||
* @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()`
|
||||
* @return array Array with a `query` string and a `bindings` array
|
||||
*/
|
||||
public function createTable(string $table, array $columns = []): array
|
||||
{
|
||||
$inner = $this->createTableInner($columns);
|
||||
|
||||
// add keys
|
||||
foreach ($inner['keys'] as $key => $columns) {
|
||||
// quote each column name and make a list string out of the column names
|
||||
$columns = implode(', ', array_map(function ($name) {
|
||||
return $this->quoteIdentifier($name);
|
||||
}, $columns));
|
||||
|
||||
if ($key === 'primary') {
|
||||
$key = 'PRIMARY KEY';
|
||||
} else {
|
||||
$unique = isset($inner['unique'][$key]) === true ? 'UNIQUE ' : '';
|
||||
$key = $unique . 'INDEX ' . $this->quoteIdentifier($key);
|
||||
}
|
||||
|
||||
$inner['query'] .= ',' . PHP_EOL . $key . ' (' . $columns . ')';
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner['query'] . PHP_EOL . ')',
|
||||
'bindings' => $inner['bindings']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a DELETE clause
|
||||
*
|
||||
* @param array $params List of parameters for the DELETE clause. See defaults for more info.
|
||||
* @return array
|
||||
*/
|
||||
public function delete(array $params = []): array
|
||||
@@ -546,19 +573,18 @@ class Sql
|
||||
|
||||
/**
|
||||
* Quotes an identifier (table *or* column)
|
||||
* Default version for MySQL
|
||||
*
|
||||
* @param $identifier string
|
||||
* @return string
|
||||
*/
|
||||
public function quoteIdentifier(string $identifier): string
|
||||
{
|
||||
// * is special
|
||||
// * is special, don't quote that
|
||||
if ($identifier === '*') {
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
// replace every backtick with two backticks
|
||||
// escape backticks inside the identifier name
|
||||
$identifier = str_replace('`', '``', $identifier);
|
||||
|
||||
// wrap in backticks
|
||||
@@ -688,22 +714,12 @@ class Sql
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of tables for a specified database
|
||||
* MySQL version
|
||||
* Returns a query to list the tables of the current database;
|
||||
* the query needs to return rows with a column `name`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function tables(): array
|
||||
{
|
||||
$binding = $this->bindingName('database');
|
||||
|
||||
return [
|
||||
'query' => 'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ' . $binding,
|
||||
'bindings' => [
|
||||
$binding => $this->database->name()
|
||||
]
|
||||
];
|
||||
}
|
||||
abstract public function tables(): array;
|
||||
|
||||
/**
|
||||
* Validates and quotes a table name
|
||||
@@ -781,10 +797,12 @@ class Sql
|
||||
* @param string $table
|
||||
* @param string $column
|
||||
* @return bool
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the column is invalid
|
||||
*/
|
||||
public function validateColumn(string $table, string $column): bool
|
||||
{
|
||||
if ($this->database->validateColumn($table, $column) === false) {
|
||||
if ($this->database->validateColumn($table, $column) !== true) {
|
||||
throw new InvalidArgumentException('Invalid column ' . $column);
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@ namespace Kirby\Database\Sql;
|
||||
use Kirby\Database\Sql;
|
||||
|
||||
/**
|
||||
* Mysql query builder
|
||||
* MySQL query builder
|
||||
*
|
||||
* @package Kirby Database
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
@@ -15,4 +15,45 @@ use Kirby\Database\Sql;
|
||||
*/
|
||||
class Mysql extends Sql
|
||||
{
|
||||
/**
|
||||
* Returns a query to list the columns of a specified table;
|
||||
* the query needs to return rows with a column `name`
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @return array
|
||||
*/
|
||||
public function columns(string $table): array
|
||||
{
|
||||
$databaseBinding = $this->bindingName('database');
|
||||
$tableBinding = $this->bindingName('table');
|
||||
|
||||
$query = 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS ';
|
||||
$query .= 'WHERE TABLE_SCHEMA = ' . $databaseBinding . ' AND TABLE_NAME = ' . $tableBinding;
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'bindings' => [
|
||||
$databaseBinding => $this->database->name(),
|
||||
$tableBinding => $table,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query to list the tables of the current database;
|
||||
* the query needs to return rows with a column `name`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function tables(): array
|
||||
{
|
||||
$binding = $this->bindingName('database');
|
||||
|
||||
return [
|
||||
'query' => 'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ' . $binding,
|
||||
'bindings' => [
|
||||
$binding => $this->database->name()
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ namespace Kirby\Database\Sql;
|
||||
use Kirby\Database\Sql;
|
||||
|
||||
/**
|
||||
* Sqlite query builder
|
||||
* SQLite query builder
|
||||
*
|
||||
* @package Kirby Database
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
@@ -16,11 +16,11 @@ use Kirby\Database\Sql;
|
||||
class Sqlite extends Sql
|
||||
{
|
||||
/**
|
||||
* Returns a list of columns for a specified table
|
||||
* SQLite version
|
||||
* Returns a query to list the columns of a specified table;
|
||||
* the query needs to return rows with a column `name`
|
||||
*
|
||||
* @param string $table The table name
|
||||
* @return string
|
||||
* @param string $table Table name
|
||||
* @return array
|
||||
*/
|
||||
public function columns(string $table): array
|
||||
{
|
||||
@@ -30,30 +30,10 @@ class Sqlite extends Sql
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional key definition for the column.
|
||||
*
|
||||
* @param array $column
|
||||
* @return array
|
||||
*/
|
||||
public function columnKey(array $column): array
|
||||
{
|
||||
if (isset($column['key']) === false || $column['key'] === 'INDEX') {
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $column['key'],
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracted column types to simplify table
|
||||
* creation for multiple database drivers
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
@@ -61,33 +41,72 @@ class Sqlite extends Sql
|
||||
{
|
||||
return [
|
||||
'id' => '{{ name }} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE',
|
||||
'varchar' => '{{ name }} TEXT {{ null }} {{ key }} {{ default }}',
|
||||
'text' => '{{ name }} TEXT {{ null }} {{ key }} {{ default }}',
|
||||
'int' => '{{ name }} INTEGER {{ null }} {{ key }} {{ default }}',
|
||||
'timestamp' => '{{ name }} INTEGER {{ null }} {{ key }} {{ default }}'
|
||||
'varchar' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}',
|
||||
'text' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}',
|
||||
'int' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}',
|
||||
'timestamp' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines an identifier (table and column)
|
||||
* SQLite version
|
||||
*
|
||||
* @param $table string
|
||||
* @param $column string
|
||||
* @param $values boolean Whether the identifier is going to be used for a values clause
|
||||
* Only relevant for SQLite
|
||||
* @param $values boolean Whether the identifier is going to be used for a VALUES clause;
|
||||
* only relevant for SQLite
|
||||
* @return string
|
||||
*/
|
||||
public function combineIdentifier(string $table, string $column, bool $values = false): string
|
||||
{
|
||||
// SQLite doesn't support qualified column names for VALUES clauses
|
||||
if ($values) {
|
||||
if ($values === true) {
|
||||
return $this->quoteIdentifier($column);
|
||||
}
|
||||
|
||||
return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CREATE TABLE query
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()`
|
||||
* @return array Array with a `query` string and a `bindings` array
|
||||
*/
|
||||
public function createTable(string $table, array $columns = []): array
|
||||
{
|
||||
$inner = $this->createTableInner($columns);
|
||||
|
||||
// add keys
|
||||
$keys = [];
|
||||
foreach ($inner['keys'] as $key => $columns) {
|
||||
// quote each column name and make a list string out of the column names
|
||||
$columns = implode(', ', array_map(function ($name) {
|
||||
return $this->quoteIdentifier($name);
|
||||
}, $columns));
|
||||
|
||||
if ($key === 'primary') {
|
||||
$inner['query'] .= ',' . PHP_EOL . 'PRIMARY KEY (' . $columns . ')';
|
||||
} else {
|
||||
// SQLite only supports index creation using a separate CREATE INDEX query
|
||||
$unique = isset($inner['unique'][$key]) === true ? 'UNIQUE ' : '';
|
||||
$keys[] = 'CREATE ' . $unique . 'INDEX ' . $this->quoteIdentifier($table . '_index_' . $key) .
|
||||
' ON ' . $this->quoteIdentifier($table) . ' (' . $columns . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$query = 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner['query'] . PHP_EOL . ')';
|
||||
if (empty($keys) === false) {
|
||||
$query .= ';' . PHP_EOL . implode(';' . PHP_EOL, $keys);
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'bindings' => $inner['bindings']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes an identifier (table *or* column)
|
||||
*
|
||||
@@ -101,7 +120,7 @@ class Sqlite extends Sql
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
// replace every quote with two quotes
|
||||
// escape quotes inside the identifier name
|
||||
$identifier = str_replace('"', '""', $identifier);
|
||||
|
||||
// wrap in quotes
|
||||
@@ -109,10 +128,9 @@ class Sqlite extends Sql
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of tables of the database
|
||||
* SQLite version
|
||||
* Returns a query to list the tables of the current database;
|
||||
* the query needs to return rows with a column `name`
|
||||
*
|
||||
* @param string $database The database name
|
||||
* @return string
|
||||
*/
|
||||
public function tables(): array
|
||||
|
@@ -5,6 +5,7 @@ namespace Kirby\Form;
|
||||
use Exception;
|
||||
use Kirby\Cms\Model;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Component;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\V;
|
||||
@@ -324,8 +325,12 @@ class Field extends Component
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->validate) === false) {
|
||||
$errors = V::errors($this->value(), $this->validate);
|
||||
if (
|
||||
empty($this->validate) === false &&
|
||||
($this->isEmpty() === false || $this->isRequired() === true)
|
||||
) {
|
||||
$rules = A::wrap($this->validate);
|
||||
$errors = V::errors($this->value(), $rules);
|
||||
|
||||
if (empty($errors) === false) {
|
||||
$this->errors = array_merge($this->errors, $errors);
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Kirby\Form;
|
||||
|
||||
use Kirby\Data\Yaml;
|
||||
use Kirby\Data\Data;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -154,7 +154,7 @@ class Form
|
||||
if ($value === null) {
|
||||
$strings[$key] = null;
|
||||
} elseif (is_array($value) === true) {
|
||||
$strings[$key] = Yaml::encode($value);
|
||||
$strings[$key] = Data::encode($value, 'yaml');
|
||||
} else {
|
||||
$strings[$key] = $value;
|
||||
}
|
||||
|
@@ -3,7 +3,10 @@
|
||||
namespace Kirby\Form;
|
||||
|
||||
use Kirby\Cms\Nest;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Http\Remote;
|
||||
use Kirby\Http\Url;
|
||||
use Kirby\Toolkit\Properties;
|
||||
use Kirby\Toolkit\Query;
|
||||
use Kirby\Toolkit\Str;
|
||||
@@ -56,14 +59,31 @@ class OptionsApi
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
$content = @file_get_contents($this->url());
|
||||
if (Url::isAbsolute($this->url()) === true) {
|
||||
// URL, request via cURL
|
||||
$data = Remote::get($this->url())->json();
|
||||
} else {
|
||||
// local file, get contents locally
|
||||
|
||||
if (empty($content) === true) {
|
||||
return [];
|
||||
// ensure the file exists before trying to load it as the
|
||||
// file_get_contents() warnings need to be suppressed
|
||||
if (is_file($this->url()) !== true) {
|
||||
throw new Exception('Local file ' . $this->url() . ' was not found');
|
||||
}
|
||||
|
||||
$content = @file_get_contents($this->url());
|
||||
|
||||
if (is_string($content) !== true) {
|
||||
throw new Exception('Unexpected read error'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (empty($content) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = json_decode($content, true);
|
||||
}
|
||||
|
||||
$data = json_decode($content, true);
|
||||
|
||||
if (is_array($data) === false) {
|
||||
throw new InvalidArgumentException('Invalid options format');
|
||||
}
|
||||
|
@@ -35,18 +35,19 @@ class Cookie
|
||||
* @param string $key The name of the cookie
|
||||
* @param string $value The cookie content
|
||||
* @param array $options Array of options:
|
||||
* lifetime, path, domain, secure, httpOnly
|
||||
* lifetime, path, domain, secure, httpOnly, sameSite
|
||||
* @return bool true: cookie was created,
|
||||
* false: cookie creation failed
|
||||
*/
|
||||
public static function set(string $key, string $value, array $options = []): bool
|
||||
{
|
||||
// extract options
|
||||
$lifetime = $options['lifetime'] ?? 0;
|
||||
$expires = static::lifetime($options['lifetime'] ?? 0);
|
||||
$path = $options['path'] ?? '/';
|
||||
$domain = $options['domain'] ?? null;
|
||||
$secure = $options['secure'] ?? false;
|
||||
$httpOnly = $options['httpOnly'] ?? true;
|
||||
$httponly = $options['httpOnly'] ?? true;
|
||||
$samesite = $options['sameSite'] ?? 'Lax';
|
||||
|
||||
// add an HMAC signature of the value
|
||||
$value = static::hmac($value) . '+' . $value;
|
||||
@@ -55,7 +56,14 @@ class Cookie
|
||||
$_COOKIE[$key] = $value;
|
||||
|
||||
// store the cookie
|
||||
return setcookie($key, $value, static::lifetime($lifetime), $path, $domain, $secure, $httpOnly);
|
||||
// the array syntax is only supported by PHP 7.3+
|
||||
// TODO: Always use the first alternative when support for PHP 7.2 is dropped
|
||||
if (version_compare(PHP_VERSION, '7.3.0', '>=') === true) {
|
||||
$options = compact('expires', 'path', 'domain', 'secure', 'httponly', 'samesite');
|
||||
return setcookie($key, $value, $options);
|
||||
} else {
|
||||
return setcookie($key, $value, $expires, $path, $domain, $secure, $httponly);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -300,7 +300,7 @@ class Header
|
||||
$options = array_merge($defaults, $params);
|
||||
|
||||
header('Pragma: public');
|
||||
header('Expires: 0');
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $options['modified']) . ' GMT');
|
||||
header('Content-Disposition: attachment; filename="' . $options['name'] . '"');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
|
@@ -3,6 +3,8 @@
|
||||
namespace Kirby\Http;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
@@ -18,6 +20,9 @@ use Kirby\Toolkit\Str;
|
||||
*/
|
||||
class Remote
|
||||
{
|
||||
const CA_INTERNAL = 1;
|
||||
const CA_SYSTEM = 2;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@@ -25,6 +30,7 @@ class Remote
|
||||
'agent' => null,
|
||||
'basicAuth' => null,
|
||||
'body' => true,
|
||||
'ca' => self::CA_INTERNAL,
|
||||
'data' => [],
|
||||
'encoding' => 'utf-8',
|
||||
'file' => null,
|
||||
@@ -96,8 +102,17 @@ class Remote
|
||||
*/
|
||||
public function __construct(string $url, array $options = [])
|
||||
{
|
||||
$defaults = static::$defaults;
|
||||
|
||||
// update the defaults with App config if set;
|
||||
// request the App instance lazily
|
||||
$app = App::instance(null, true);
|
||||
if ($app !== null) {
|
||||
$defaults = array_merge($defaults, $app->option('remote', []));
|
||||
}
|
||||
|
||||
// set all options
|
||||
$this->options = array_merge(static::$defaults, $options);
|
||||
$this->options = array_merge($defaults, $options);
|
||||
|
||||
// add the url
|
||||
$this->options['url'] = $url;
|
||||
@@ -138,7 +153,6 @@ class Remote
|
||||
*/
|
||||
public function fetch()
|
||||
{
|
||||
|
||||
// curl options
|
||||
$this->curlopt = [
|
||||
CURLOPT_URL => $this->options['url'],
|
||||
@@ -149,7 +163,6 @@ class Remote
|
||||
CURLOPT_RETURNTRANSFER => $this->options['body'],
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_HEADER => false,
|
||||
CURLOPT_HEADERFUNCTION => function ($curl, $header) {
|
||||
$parts = Str::split($header, ':');
|
||||
@@ -163,6 +176,24 @@ class Remote
|
||||
}
|
||||
];
|
||||
|
||||
// determine the TLS CA to use
|
||||
if (is_file($this->options['ca']) === true) {
|
||||
$this->curlopt[CURLOPT_SSL_VERIFYPEER] = true;
|
||||
$this->curlopt[CURLOPT_CAINFO] = $this->options['ca'];
|
||||
} elseif (is_dir($this->options['ca']) === true) {
|
||||
$this->curlopt[CURLOPT_SSL_VERIFYPEER] = true;
|
||||
$this->curlopt[CURLOPT_CAPATH] = $this->options['ca'];
|
||||
} elseif ($this->options['ca'] === self::CA_INTERNAL) {
|
||||
$this->curlopt[CURLOPT_SSL_VERIFYPEER] = true;
|
||||
$this->curlopt[CURLOPT_CAINFO] = dirname(__DIR__, 2) . '/cacert.pem';
|
||||
} elseif ($this->options['ca'] === self::CA_SYSTEM) {
|
||||
$this->curlopt[CURLOPT_SSL_VERIFYPEER] = true;
|
||||
} elseif ($this->options['ca'] === false) {
|
||||
$this->curlopt[CURLOPT_SSL_VERIFYPEER] = false;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid "ca" option for the Remote class');
|
||||
}
|
||||
|
||||
// add the progress
|
||||
if (is_callable($this->options['progress']) === true) {
|
||||
$this->curlopt[CURLOPT_NOPROGRESS] = false;
|
||||
|
@@ -107,7 +107,7 @@ class Request
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = $options;
|
||||
$this->method = $options['method'] ?? $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
$this->method = $this->detectRequestMethod($options['method'] ?? null);
|
||||
|
||||
if (isset($options['body']) === true) {
|
||||
$this->body = new Body($options['body']);
|
||||
@@ -208,6 +208,39 @@ class Request
|
||||
return array_merge($this->body()->toArray(), $this->query()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the request method from various
|
||||
* options: given method, query string, server vars
|
||||
*
|
||||
* @param string $method
|
||||
* @return string
|
||||
*/
|
||||
public function detectRequestMethod(string $method = null): string
|
||||
{
|
||||
// all possible methods
|
||||
$methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'];
|
||||
|
||||
// the request method can be overwritten with a header
|
||||
$methodOverride = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ?? null);
|
||||
|
||||
if ($method === null && in_array($methodOverride, $methods) === true) {
|
||||
$method = $methodOverride;
|
||||
}
|
||||
|
||||
// final chain of options to detect the method
|
||||
$method = $method ?? $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
// uppercase the shit out of it
|
||||
$method = strtoupper($method);
|
||||
|
||||
// sanitize the method
|
||||
if (in_array($method, $methods) === false) {
|
||||
$method = 'GET';
|
||||
}
|
||||
|
||||
return $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the domain
|
||||
*
|
||||
|
@@ -151,9 +151,10 @@ class Response
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $filename
|
||||
* @param array $props Custom overrides for response props (e.g. headers)
|
||||
* @return self
|
||||
*/
|
||||
public static function download(string $file, string $filename = null)
|
||||
public static function download(string $file, string $filename = null, array $props = [])
|
||||
{
|
||||
if (file_exists($file) === false) {
|
||||
throw new Exception('The file could not be found');
|
||||
@@ -164,19 +165,21 @@ class Response
|
||||
$body = file_get_contents($file);
|
||||
$size = strlen($body);
|
||||
|
||||
return new static([
|
||||
$props = array_replace_recursive([
|
||||
'body' => $body,
|
||||
'type' => 'application/force-download',
|
||||
'headers' => [
|
||||
'Pragma' => 'public',
|
||||
'Expires' => '0',
|
||||
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
||||
'Last-Modified' => gmdate('D, d M Y H:i:s', $modified) . ' GMT',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
'Content-Transfer-Encoding' => 'binary',
|
||||
'Content-Length' => $size,
|
||||
'Connection' => 'close'
|
||||
]
|
||||
]);
|
||||
], $props);
|
||||
|
||||
return new static($props);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,11 +187,17 @@ class Response
|
||||
* sends the file content to the browser
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $props Custom overrides for response props (e.g. headers)
|
||||
* @return self
|
||||
*/
|
||||
public static function file(string $file)
|
||||
public static function file(string $file, array $props = [])
|
||||
{
|
||||
return new static(F::read($file), F::extensionToMime(F::extension($file)));
|
||||
$props = array_merge([
|
||||
'body' => F::read($file),
|
||||
'type' => F::extensionToMime(F::extension($file))
|
||||
], $props);
|
||||
|
||||
return new static($props);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,12 +254,12 @@ class Response
|
||||
* @param int $code
|
||||
* @return self
|
||||
*/
|
||||
public static function redirect(?string $location = null, ?int $code = null)
|
||||
public static function redirect(string $location = '/', int $code = 302)
|
||||
{
|
||||
return new static([
|
||||
'code' => $code ?? 302,
|
||||
'code' => $code,
|
||||
'headers' => [
|
||||
'Location' => Url::unIdn($location ?? '/')
|
||||
'Location' => Url::unIdn($location)
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
@@ -158,7 +158,7 @@ class Route
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function next(): void
|
||||
public static function next(): void
|
||||
{
|
||||
throw new Exceptions\NextRouteException('next');
|
||||
}
|
||||
|
@@ -107,13 +107,14 @@ class Router
|
||||
$result = $route->action()->call($route, ...$route->arguments());
|
||||
}
|
||||
|
||||
$loop = false;
|
||||
$loop = false;
|
||||
} catch (Exceptions\NextRouteException $e) {
|
||||
$ignore[] = $route;
|
||||
}
|
||||
|
||||
if (is_a(static::$afterEach, 'Closure') === true) {
|
||||
$result = (static::$afterEach)($route, $path, $method, $result);
|
||||
$final = $loop === false;
|
||||
$result = (static::$afterEach)($route, $path, $method, $result, $final);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -119,8 +119,8 @@ class Url
|
||||
// matches the following groups of URLs:
|
||||
// //example.com/uri
|
||||
// http://example.com/uri, https://example.com/uri, ftp://example.com/uri
|
||||
// mailto:example@example.com
|
||||
return preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:)!i', $url) === 1;
|
||||
// mailto:example@example.com, geo:49.0158,8.3239?z=11
|
||||
return preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:|geo:)!i', $url) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -34,8 +34,8 @@ class Camera
|
||||
*/
|
||||
public function __construct(array $exif)
|
||||
{
|
||||
$this->make = @$exif['Make'];
|
||||
$this->model = @$exif['Model'];
|
||||
$this->make = $exif['Make'] ?? null;
|
||||
$this->model = $exif['Model'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -459,7 +459,8 @@ class Session
|
||||
'lifetime' => $this->tokenExpiry,
|
||||
'path' => Url::index(['host' => null, 'trailingSlash' => true]),
|
||||
'secure' => Url::scheme() === 'https',
|
||||
'httpOnly' => true
|
||||
'httpOnly' => true,
|
||||
'sameSite' => 'Lax'
|
||||
]);
|
||||
} else {
|
||||
$this->needsRetransmission = true;
|
||||
|
@@ -84,7 +84,14 @@ class KirbyTag
|
||||
public static function parse(string $string, array $data = [], array $options = [])
|
||||
{
|
||||
// remove the brackets, extract the first attribute (the tag type)
|
||||
$tag = trim(rtrim(ltrim($string, '('), ')'));
|
||||
$tag = trim(ltrim($string, '('));
|
||||
|
||||
// use substr instead of rtrim to keep non-tagged brackets
|
||||
// (link: file.pdf text: Download (PDF))
|
||||
if (substr($tag, -1) === ')') {
|
||||
$tag = substr($tag, 0, -1);
|
||||
}
|
||||
|
||||
$type = trim(substr($tag, 0, strpos($tag, ':')));
|
||||
$type = strtolower($type);
|
||||
$attr = static::$types[$type]['attr'] ?? [];
|
||||
|
@@ -3,6 +3,8 @@
|
||||
namespace Kirby\Text;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Parses and converts custom kirbytags in any
|
||||
@@ -22,10 +24,31 @@ class KirbyTags
|
||||
|
||||
public static function parse(string $text = null, array $data = [], array $options = []): string
|
||||
{
|
||||
return preg_replace_callback('!(?=[^\]])\([a-z0-9_-]+:.*?\)!is', function ($match) use ($data, $options) {
|
||||
$regex = '!
|
||||
(?=[^\]]) # positive lookahead that matches a group after the main expression without including ] in the result
|
||||
(?=\([a-z0-9_-]+:) # positive lookahead that requires starts with ( and lowercase ASCII letters, digits, underscores or hyphens followed with : immediately to the right of the current location
|
||||
(\( # capturing group 1
|
||||
(?:[^()]+|(?1))*+ # repetitions of any chars other than ( and ) or the whole group 1 pattern (recursed)
|
||||
\)) # end of capturing group 1
|
||||
!isx';
|
||||
|
||||
return preg_replace_callback($regex, function ($match) use ($data, $options) {
|
||||
$debug = $options['debug'] ?? false;
|
||||
|
||||
try {
|
||||
return static::$tagClass::parse($match[0], $data, $options)->render();
|
||||
} catch (InvalidArgumentException $e) {
|
||||
// stay silent in production and ignore non-existing tags
|
||||
if ($debug !== true || Str::startsWith($e->getMessage(), 'Undefined tag type:') === true) {
|
||||
return $match[0];
|
||||
}
|
||||
|
||||
throw $e;
|
||||
} catch (Exception $e) {
|
||||
if ($debug === true) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $match[0];
|
||||
}
|
||||
}, $text);
|
||||
|
@@ -56,11 +56,11 @@ class Markdown
|
||||
/**
|
||||
* Parses the given text and returns the HTML
|
||||
*
|
||||
* @param string $text
|
||||
* @param string|null $text
|
||||
* @param bool $inline
|
||||
* @return string
|
||||
*/
|
||||
public function parse(string $text, bool $inline = false): string
|
||||
public function parse(string $text = null, bool $inline = false): string
|
||||
{
|
||||
if ($this->options['extra'] === true) {
|
||||
$parser = new ParsedownExtra();
|
||||
|
@@ -115,10 +115,10 @@ class SmartyPants
|
||||
/**
|
||||
* Parses the given text
|
||||
*
|
||||
* @param string $text
|
||||
* @param string|null $text
|
||||
* @return string
|
||||
*/
|
||||
public function parse(string $text): string
|
||||
public function parse(string $text = null): string
|
||||
{
|
||||
// prepare the text
|
||||
$text = str_replace('"', '"', $text);
|
||||
|
@@ -81,29 +81,38 @@ class A
|
||||
return $array[$key];
|
||||
}
|
||||
|
||||
// support dot notation
|
||||
// extract data from nested array structures using the dot notation
|
||||
if (strpos($key, '.') !== false) {
|
||||
$keys = explode('.', $key);
|
||||
$firstKey = array_shift($keys);
|
||||
|
||||
// if the input array also uses dot notation, try to find a subset of the $keys
|
||||
if (isset($array[$firstKey]) === false) {
|
||||
$currentKey = $firstKey;
|
||||
|
||||
while ($innerKey = array_shift($keys)) {
|
||||
$currentKey = $currentKey . '.' . $innerKey;
|
||||
$currentKey .= '.' . $innerKey;
|
||||
|
||||
if (isset($array[$currentKey]) === true && is_array($array[$currentKey])) {
|
||||
// the element needs to exist and also needs to be an array; otherwise
|
||||
// we cannot find the remaining keys within it (invalid array structure)
|
||||
if (isset($array[$currentKey]) === true && is_array($array[$currentKey]) === true) {
|
||||
// $keys only holds the remaining keys that have not been shifted off yet
|
||||
return static::get($array[$currentKey], implode('.', $keys), $default);
|
||||
}
|
||||
}
|
||||
|
||||
// searching through the full chain of keys wasn't successful
|
||||
return $default;
|
||||
}
|
||||
|
||||
// if the input array uses a completely nested structure,
|
||||
// recursively progress layer by layer
|
||||
if (is_array($array[$firstKey]) === true) {
|
||||
return static::get($array[$firstKey], implode('.', $keys), $default);
|
||||
}
|
||||
|
||||
// the $firstKey element was found, but isn't an array, so we cannot
|
||||
// find the remaining keys within it (invalid array structure)
|
||||
return $default;
|
||||
}
|
||||
|
||||
@@ -410,6 +419,89 @@ class A
|
||||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an array into a nested form by converting
|
||||
* dot notation in keys to nested structures
|
||||
*
|
||||
* @param array $array
|
||||
* @param array $ignore List of keys in dot notation that should
|
||||
* not be converted to a nested structure
|
||||
* @return array
|
||||
*/
|
||||
public static function nest(array $array, array $ignore = []): array
|
||||
{
|
||||
// convert a simple ignore list to a nested $key => true array
|
||||
if (isset($ignore[0]) === true) {
|
||||
$ignore = array_map(function () {
|
||||
return true;
|
||||
}, array_flip($ignore));
|
||||
|
||||
$ignore = A::nest($ignore);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($array as $fullKey => $value) {
|
||||
// extract the first part of a multi-level key, keep the others
|
||||
$subKeys = explode('.', $fullKey);
|
||||
$key = array_shift($subKeys);
|
||||
|
||||
// skip the magic for ignored keys
|
||||
if (isset($ignore[$key]) === true && $ignore[$key] === true) {
|
||||
$result[$fullKey] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// untangle elements where the key uses dot notation
|
||||
if (count($subKeys) > 0) {
|
||||
$value = static::nestByKeys($value, $subKeys);
|
||||
}
|
||||
|
||||
// now recursively do the same for each array level if needed
|
||||
if (is_array($value) === true) {
|
||||
$value = static::nest($value, $ignore[$key] ?? []);
|
||||
}
|
||||
|
||||
// merge arrays with previous results if necessary
|
||||
// (needed when the same keys are used both with and without dot notation)
|
||||
if (
|
||||
isset($result[$key]) === true &&
|
||||
is_array($result[$key]) === true &&
|
||||
is_array($value) === true
|
||||
) {
|
||||
$result[$key] = array_replace_recursive($result[$key], $value);
|
||||
} else {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively creates a nested array from a set of keys
|
||||
* with a key on each level
|
||||
*
|
||||
* @param mixed $value Arbitrary value that will end up at the bottom of the tree
|
||||
* @param array $keys List of keys to use sorted from the topmost level
|
||||
* @return array|mixed Nested array or (if `$keys` is empty) the input `$value`
|
||||
*/
|
||||
public static function nestByKeys($value, array $keys)
|
||||
{
|
||||
// shift off the first key from the list
|
||||
$firstKey = array_shift($keys);
|
||||
|
||||
// stop further recursion if there are no more keys
|
||||
if ($firstKey === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// return one level of the output tree, recurse further
|
||||
return [
|
||||
$firstKey => static::nestByKeys($value, $keys)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts a multi-dimensional array by a certain column
|
||||
*
|
||||
|
@@ -26,6 +26,14 @@ class Collection extends Iterator implements Countable
|
||||
*/
|
||||
public static $filters = [];
|
||||
|
||||
/**
|
||||
* Whether the collection keys should be
|
||||
* treated as case-sensitive
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $caseSensitive = false;
|
||||
|
||||
/**
|
||||
* Pagination object
|
||||
* @var Pagination
|
||||
@@ -48,9 +56,12 @@ class Collection extends Iterator implements Countable
|
||||
* Constructor
|
||||
*
|
||||
* @param array $data
|
||||
* @param bool $caseSensitive Whether the collection keys should be
|
||||
* treated as case-sensitive
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
public function __construct(array $data = [], bool $caseSensitive = false)
|
||||
{
|
||||
$this->caseSensitive = $caseSensitive;
|
||||
$this->set($data);
|
||||
}
|
||||
|
||||
@@ -72,11 +83,11 @@ class Collection extends Iterator implements Countable
|
||||
*/
|
||||
public function __get($key)
|
||||
{
|
||||
if (isset($this->data[$key])) {
|
||||
return $this->data[$key];
|
||||
if ($this->caseSensitive === true) {
|
||||
return $this->data[$key] ?? null;
|
||||
}
|
||||
|
||||
return $this->data[strtolower($key)] ?? null;
|
||||
return $this->data[$key] ?? $this->data[strtolower($key)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,7 +98,12 @@ class Collection extends Iterator implements Countable
|
||||
*/
|
||||
public function __set(string $key, $value)
|
||||
{
|
||||
$this->data[strtolower($key)] = $value;
|
||||
if ($this->caseSensitive === true) {
|
||||
$this->data[$key] = $value;
|
||||
} else {
|
||||
$this->data[strtolower($key)] = $value;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -1243,7 +1259,7 @@ Collection::$filters['!^='] = [
|
||||
/**
|
||||
* Between Filter
|
||||
*/
|
||||
Collection::$filters['between'] = [
|
||||
Collection::$filters['between'] = Collection::$filters['..'] = [
|
||||
'validator' => function ($value, $test) {
|
||||
return V::between($value, ...$test) === true;
|
||||
},
|
||||
@@ -1294,3 +1310,67 @@ Collection::$filters['maxwords'] = [
|
||||
Collection::$filters['minwords'] = [
|
||||
'validator' => 'V::minWords',
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Equals Filter
|
||||
*/
|
||||
Collection::$filters['date =='] = [
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '==', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Not Equals Filter
|
||||
*/
|
||||
Collection::$filters['date !='] = [
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '!=', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date More Filter
|
||||
*/
|
||||
Collection::$filters['date >'] = [
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '>', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Min Filter
|
||||
*/
|
||||
Collection::$filters['date >='] = [
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '>=', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Less Filter
|
||||
*/
|
||||
Collection::$filters['date <'] = [
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '<', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Max Filter
|
||||
*/
|
||||
Collection::$filters['date <='] = [
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '<=', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Between Filter
|
||||
*/
|
||||
Collection::$filters['date between'] = Collection::$filters['date ..'] = [
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '>=', $test[0]) &&
|
||||
V::date($value, '<=', $test[1]);
|
||||
}
|
||||
];
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use ArgumentCountError;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use TypeError;
|
||||
|
||||
@@ -224,8 +225,12 @@ class Component
|
||||
$definition = static::$types[$type];
|
||||
|
||||
// load definitions from string
|
||||
if (is_array($definition) === false) {
|
||||
static::$types[$type] = $definition = include $definition;
|
||||
if (is_string($definition) === true) {
|
||||
if (is_file($definition) !== true) {
|
||||
throw new Exception('Component definition ' . $definition . ' does not exist');
|
||||
}
|
||||
|
||||
static::$types[$type] = $definition = F::load($definition);
|
||||
}
|
||||
|
||||
return $definition;
|
||||
|
@@ -51,11 +51,11 @@ class Controller
|
||||
|
||||
public static function load(string $file)
|
||||
{
|
||||
if (file_exists($file) === false) {
|
||||
if (is_file($file) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$function = require $file;
|
||||
$function = F::load($file);
|
||||
|
||||
if (is_a($function, 'Closure') === false) {
|
||||
return null;
|
||||
|
@@ -99,7 +99,7 @@ class Dir
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the directory exsits on disk
|
||||
* Checks if the directory exists on disk
|
||||
*
|
||||
* @param string $dir
|
||||
* @return bool
|
||||
|
@@ -103,7 +103,7 @@ class F
|
||||
],
|
||||
];
|
||||
|
||||
public static $units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
public static $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
/**
|
||||
* Appends new content to an existing file
|
||||
@@ -357,19 +357,24 @@ class F
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file and returns the result
|
||||
* Loads a file and returns the result or `false` if the
|
||||
* file to load does not exist
|
||||
*
|
||||
* @param string $file
|
||||
* @param mixed $fallback
|
||||
* @param array $data Optional array of variables to extract in the variable scope
|
||||
* @return mixed
|
||||
*/
|
||||
public static function load(string $file, $fallback = null)
|
||||
public static function load(string $file, $fallback = null, array $data = [])
|
||||
{
|
||||
if (file_exists($file) === false) {
|
||||
if (is_file($file) === false) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$result = include $file;
|
||||
// we use the loadIsolated() method here to prevent the included
|
||||
// file from overwriting our $fallback in this variable scope; see
|
||||
// https://www.php.net/manual/en/function.include.php#example-124
|
||||
$result = static::loadIsolated($file, $data);
|
||||
|
||||
if ($fallback !== null && gettype($result) !== gettype($fallback)) {
|
||||
return $fallback;
|
||||
@@ -378,6 +383,39 @@ class F
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file with as little as possible in the variable scope
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $data Optional array of variables to extract in the variable scope
|
||||
* @return mixed
|
||||
*/
|
||||
protected static function loadIsolated(string $file, array $data = [])
|
||||
{
|
||||
// extract the $data variables in this scope to be accessed by the included file;
|
||||
// protect $file against being overwritten by a $data variable
|
||||
$___file___ = $file;
|
||||
extract($data);
|
||||
|
||||
return include $___file___;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file using `include_once()` and returns whether loading was successful
|
||||
*
|
||||
* @param string $file
|
||||
* @return bool
|
||||
*/
|
||||
public static function loadOnce(string $file): bool
|
||||
{
|
||||
if (is_file($file) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
include_once $file;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type of a file
|
||||
*
|
||||
@@ -498,7 +536,7 @@ class F
|
||||
|
||||
// avoid errors for invalid sizes
|
||||
if ($size <= 0) {
|
||||
return '0 kB';
|
||||
return '0 KB';
|
||||
}
|
||||
|
||||
// the math magic
|
||||
|
@@ -6,7 +6,7 @@ use Exception;
|
||||
use Kirby\Http\Url;
|
||||
|
||||
/**
|
||||
* Html builder for the most common elements
|
||||
* HTML builder for the most common elements
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
@@ -14,58 +14,60 @@ use Kirby\Http\Url;
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Html
|
||||
class Html extends Xml
|
||||
{
|
||||
/**
|
||||
* An internal store for a html entities translation table
|
||||
* An internal store for an HTML entities translation table
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $entities;
|
||||
|
||||
/**
|
||||
* Can be used to switch to trailing slashes if required
|
||||
* Closing string for void tags;
|
||||
* can be used to switch to trailing slashes if required
|
||||
*
|
||||
* ```php
|
||||
* html::$void = ' />'
|
||||
* Html::$void = ' />'
|
||||
* ```
|
||||
*
|
||||
* @var string $void
|
||||
* @var string
|
||||
*/
|
||||
public static $void = '>';
|
||||
|
||||
/**
|
||||
* Generic HTML tag generator
|
||||
* Can be called like `Html::p('A paragraph', ['class' => 'text'])`
|
||||
*
|
||||
* @param string $tag
|
||||
* @param array $arguments
|
||||
* @param string $tag Tag name
|
||||
* @param array $arguments Further arguments for the Html::tag() method
|
||||
* @return string
|
||||
*/
|
||||
public static function __callStatic(string $tag, array $arguments = []): string
|
||||
{
|
||||
if (static::isVoid($tag) === true) {
|
||||
return Html::tag($tag, null, ...$arguments);
|
||||
return static::tag($tag, null, ...$arguments);
|
||||
}
|
||||
|
||||
return Html::tag($tag, ...$arguments);
|
||||
return static::tag($tag, ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an `a` tag
|
||||
* Generates an `<a>` tag; automatically supports mailto: and tel: links
|
||||
*
|
||||
* @param string $href The url for the `a` tag
|
||||
* @param mixed $text The optional text. If `null`, the url will be used as text
|
||||
* @param string $href The URL for the `<a>` tag
|
||||
* @param string|array|null $text The optional text; if `null`, the URL will be used as text
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string the generated html
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function a(string $href = null, $text = null, array $attr = []): string
|
||||
public static function a(string $href, $text = null, array $attr = []): string
|
||||
{
|
||||
if (Str::startsWith($href, 'mailto:')) {
|
||||
return static::email($href, $text, $attr);
|
||||
return static::email(substr($href, 7), $text, $attr);
|
||||
}
|
||||
|
||||
if (Str::startsWith($href, 'tel:')) {
|
||||
return static::tel($href, $text, $attr);
|
||||
return static::tel(substr($href, 4), $text, $attr);
|
||||
}
|
||||
|
||||
return static::link($href, $text, $attr);
|
||||
@@ -74,92 +76,47 @@ class Html
|
||||
/**
|
||||
* Generates a single attribute or a list of attributes
|
||||
*
|
||||
* @param string $name mixed string: a single attribute with that name will be generated. array: a list of attributes will be generated. Don't pass a second argument in that case.
|
||||
* @param string $value if used for a single attribute, pass the content for the attribute here
|
||||
* @return string the generated html
|
||||
* @param string|array $name String: A single attribute with that name will be generated.
|
||||
* Key-value array: A list of attributes will be generated. Don't pass a second argument in that case.
|
||||
* @param mixed $value If used with a `$name` string, pass the value of the attribute here.
|
||||
* If used with a `$name` array, this can be set to `false` to disable attribute sorting.
|
||||
* @return string|null The generated HTML attributes string
|
||||
*/
|
||||
public static function attr($name, $value = null): string
|
||||
public static function attr($name, $value = null): ?string
|
||||
{
|
||||
if (is_array($name) === true) {
|
||||
$attributes = [];
|
||||
|
||||
ksort($name);
|
||||
|
||||
foreach ($name as $key => $val) {
|
||||
$a = static::attr($key, $val);
|
||||
|
||||
if ($a) {
|
||||
$attributes[] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $attributes);
|
||||
// HTML supports boolean attributes without values
|
||||
if (is_array($name) === false && is_bool($value) === true) {
|
||||
return $value === true ? strtolower($name) : null;
|
||||
}
|
||||
|
||||
if ($value === null || $value === '' || $value === []) {
|
||||
return false;
|
||||
}
|
||||
// all other cases can share the XML variant
|
||||
$attr = parent::attr($name, $value);
|
||||
|
||||
if ($value === ' ') {
|
||||
return strtolower($name) . '=""';
|
||||
}
|
||||
|
||||
if (is_bool($value) === true) {
|
||||
return $value === true ? strtolower($name) : '';
|
||||
}
|
||||
|
||||
if (is_array($value) === true) {
|
||||
if (isset($value['value'], $value['escape'])) {
|
||||
$value = $value['escape'] === true ? htmlspecialchars($value['value'], ENT_QUOTES, 'UTF-8') : $value['value'];
|
||||
} else {
|
||||
$value = implode(' ', array_filter($value, function ($value) {
|
||||
return !empty($value) || is_numeric($value);
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
return strtolower($name) . '="' . $value . '"';
|
||||
// HTML supports named entities
|
||||
$entities = parent::entities();
|
||||
$html = array_keys($entities);
|
||||
$xml = array_values($entities);
|
||||
return str_replace($xml, $html, $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts lines in a string into html breaks
|
||||
* Converts lines in a string into HTML breaks
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function breaks(string $string = null): string
|
||||
public static function breaks(string $string): string
|
||||
{
|
||||
return nl2br($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all html tags and encoded chars from a string
|
||||
* Generates an `<a>` tag with `mailto:`
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* echo html::decode('some uber <em>crazy</em> stuff');
|
||||
* // output: some uber crazy stuff
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string The html string
|
||||
*/
|
||||
public static function decode(string $string = null): string
|
||||
{
|
||||
$string = strip_tags($string);
|
||||
return html_entity_decode($string, ENT_COMPAT, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an `a` tag with `mailto:`
|
||||
*
|
||||
* @param string $email The url for the a tag
|
||||
* @param mixed $text The optional text. If null, the url will be used as text
|
||||
* @param string $email The email address
|
||||
* @param string|array|null $text The optional text; if `null`, the email address will be used as text
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string the generated html
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function email(string $email, $text = null, array $attr = []): string
|
||||
{
|
||||
@@ -168,8 +125,10 @@ class Html
|
||||
}
|
||||
|
||||
if (empty($text) === true) {
|
||||
// show only the eMail address without additional parameters (if the 'text' argument is empty)
|
||||
$text = [Str::encode(Str::split($email, '?')[0])];
|
||||
// show only the email address without additional parameters
|
||||
$address = Str::contains($email, '?') ? Str::before($email, '?') : $email;
|
||||
|
||||
$text = [Str::encode($address)];
|
||||
}
|
||||
|
||||
$email = Str::encode($email);
|
||||
@@ -187,14 +146,18 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to a html-safe string
|
||||
* Converts a string to an HTML-safe string
|
||||
*
|
||||
* @param string $string
|
||||
* @param bool $keepTags
|
||||
* @return string The html string
|
||||
* @param string|null $string
|
||||
* @param bool $keepTags If true, existing tags won't be escaped
|
||||
* @return string The HTML string
|
||||
*/
|
||||
public static function encode(string $string = null, bool $keepTags = false): string
|
||||
public static function encode(?string $string, bool $keepTags = false): string
|
||||
{
|
||||
if ($string === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($keepTags === true) {
|
||||
$list = static::entities();
|
||||
unset($list['"'], $list['<'], $list['>'], $list['&']);
|
||||
@@ -209,24 +172,24 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entities translation table
|
||||
* Returns the entity translation table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function entities(): array
|
||||
{
|
||||
return static::$entities = static::$entities ?? get_html_translation_table(HTML_ENTITIES);
|
||||
return self::$entities = self::$entities ?? get_html_translation_table(HTML_ENTITIES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a figure tag with optional caption
|
||||
* Creates a `<figure>` tag with optional caption
|
||||
*
|
||||
* @param string|array $content
|
||||
* @param string|array $caption
|
||||
* @param array $attr
|
||||
* @return string
|
||||
* @param string|array $content Contents of the `<figure>` tag
|
||||
* @param string|array $caption Optional `<figcaption>` text to use
|
||||
* @param array $attr Additional attributes for the `<figure>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function figure($content, $caption = null, array $attr = []): string
|
||||
public static function figure($content, $caption = '', array $attr = []): string
|
||||
{
|
||||
if ($caption) {
|
||||
$figcaption = static::tag('figcaption', $caption);
|
||||
@@ -242,14 +205,14 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a gist
|
||||
* Embeds a GitHub Gist
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $file
|
||||
* @param array $attr
|
||||
* @return string
|
||||
* @param string $url Gist URL
|
||||
* @param string|null $file Optional specific file to embed
|
||||
* @param array $attr Additional attributes for the `<script>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function gist(string $url, string $file = null, array $attr = []): string
|
||||
public static function gist(string $url, ?string $file = null, array $attr = []): string
|
||||
{
|
||||
if ($file === null) {
|
||||
$src = $url . '.js';
|
||||
@@ -257,29 +220,27 @@ class Html
|
||||
$src = $url . '.js?file=' . $file;
|
||||
}
|
||||
|
||||
return static::tag('script', null, array_merge($attr, [
|
||||
'src' => $src
|
||||
]));
|
||||
return static::tag('script', '', array_merge($attr, ['src' => $src]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an iframe
|
||||
* Creates an `<iframe>`
|
||||
*
|
||||
* @param string $src
|
||||
* @param array $attr
|
||||
* @return string
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function iframe(string $src, array $attr = []): string
|
||||
{
|
||||
return static::tag('iframe', null, array_merge(['src' => $src], $attr));
|
||||
return static::tag('iframe', '', array_merge(['src' => $src], $attr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an img tag
|
||||
* Generates an `<img>` tag
|
||||
*
|
||||
* @param string $src The url of the image
|
||||
* @param array $attr Additional attributes for the image tag
|
||||
* @return string the generated html
|
||||
* @param string $src The URL of the image
|
||||
* @param array $attr Additional attributes for the `<img>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function img(string $src, array $attr = []): string
|
||||
{
|
||||
@@ -288,7 +249,7 @@ class Html
|
||||
'alt' => ' '
|
||||
], $attr);
|
||||
|
||||
return static::tag('img', null, $attr);
|
||||
return static::tag('img', '', $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -322,14 +283,14 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an `a` link tag
|
||||
* Generates an `<a>` link tag (without automatic email: and tel: detection)
|
||||
*
|
||||
* @param string $href The url for the `a` tag
|
||||
* @param mixed $text The optional text. If `null`, the url will be used as text
|
||||
* @param string $href The URL for the `<a>` tag
|
||||
* @param string|array|null $text The optional text; if `null`, the URL will be used as text
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string the generated html
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function link(string $href = null, $text = null, array $attr = []): string
|
||||
public static function link(string $href, $text = null, array $attr = []): string
|
||||
{
|
||||
$attr = array_merge(['href' => $href], $attr);
|
||||
|
||||
@@ -348,13 +309,13 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Add noopeener noreferrer to rels when target is `_blank`
|
||||
* Add noopener & noreferrer to rels when target is `_blank`
|
||||
*
|
||||
* @param string $rel
|
||||
* @param string $target
|
||||
* @return string|null
|
||||
* @param string|null $rel Current `rel` value
|
||||
* @param string|null $target Current `target` value
|
||||
* @return string|null New `rel` value or `null` if not needed
|
||||
*/
|
||||
public static function rel(string $rel = null, string $target = null)
|
||||
public static function rel(?string $rel = null, ?string $target = null): ?string
|
||||
{
|
||||
$rel = trim($rel);
|
||||
|
||||
@@ -370,47 +331,35 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an Html tag with optional content and attributes
|
||||
* Builds an HTML tag
|
||||
*
|
||||
* @param string $name The name of the tag, i.e. `a`
|
||||
* @param mixed $content The content if availble. Pass `null` to generate a self-closing tag, Pass an empty string to generate empty content
|
||||
* @param string $name Tag name
|
||||
* @param array|string|null $content Scalar value or array with multiple lines of content or `null` to
|
||||
* generate a self-closing tag; pass an empty string to generate empty content
|
||||
* @param array $attr An associative array with additional attributes for the tag
|
||||
* @return string The generated Html
|
||||
* @param string|null $indent Indentation string, defaults to two spaces or `null` for output on one line
|
||||
* @param int $level Indentation level
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function tag(string $name, $content = null, array $attr = []): string
|
||||
public static function tag(string $name, $content = '', array $attr = null, string $indent = null, int $level = 0): string
|
||||
{
|
||||
$html = '<' . $name;
|
||||
$attr = static::attr($attr);
|
||||
|
||||
if (empty($attr) === false) {
|
||||
$html .= ' ' . $attr;
|
||||
}
|
||||
|
||||
// force void elements to be self-closing
|
||||
if (static::isVoid($name) === true) {
|
||||
$html .= static::$void;
|
||||
} else {
|
||||
if (is_array($content) === true) {
|
||||
$content = implode($content);
|
||||
} else {
|
||||
$content = static::encode($content, false);
|
||||
}
|
||||
|
||||
$html .= '>' . $content . '</' . $name . '>';
|
||||
$content = null;
|
||||
}
|
||||
|
||||
return $html;
|
||||
return parent::tag($name, $content, $attr, $indent, $level);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates an `a` tag for a phone number
|
||||
* Generates an `<a>` tag for a phone number
|
||||
*
|
||||
* @param string $tel The phone number
|
||||
* @param mixed $text The optional text. If `null`, the number will be used as text
|
||||
* @param string|array|null $text The optional text; if `null`, the phone number will be used as text
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string the generated html
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function tel($tel = null, $text = null, array $attr = []): string
|
||||
public static function tel(string $tel, $text = null, array $attr = []): string
|
||||
{
|
||||
$number = preg_replace('![^0-9\+]+!', '', $tel);
|
||||
|
||||
@@ -422,16 +371,44 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a video embed via iframe for Youtube or Vimeo
|
||||
* videos. The embed Urls are automatically detected from
|
||||
* the given URL.
|
||||
* Properly encodes tag contents
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param array $attr
|
||||
* @return string
|
||||
* @param mixed $value
|
||||
* @return string|null
|
||||
*/
|
||||
public static function video(string $url, ?array $options = [], array $attr = []): string
|
||||
public static function value($value): ?string
|
||||
{
|
||||
if ($value === true) {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
if ($value === false) {
|
||||
return 'false';
|
||||
}
|
||||
|
||||
if (is_numeric($value) === true) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::encode($value, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a video embed via `<iframe>` for YouTube or Vimeo
|
||||
* videos; the embed URLs are automatically detected from
|
||||
* the given URL
|
||||
*
|
||||
* @param string $url Video URL
|
||||
* @param array $options Additional `vimeo` and `youtube` options
|
||||
* (will be used as query params in the embed URL)
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function video(string $url, array $options = [], array $attr = []): string
|
||||
{
|
||||
// YouTube video
|
||||
if (preg_match('!youtu!i', $url) === 1) {
|
||||
@@ -447,14 +424,14 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a Vimeo video by URL in an iframe
|
||||
* Embeds a Vimeo video by URL in an `<iframe>`
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param array $attr
|
||||
* @return string
|
||||
* @param string $url Vimeo video URL
|
||||
* @param array $options Query params for the embed URL
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function vimeo(string $url, ?array $options = [], array $attr = []): string
|
||||
public static function vimeo(string $url, array $options = [], array $attr = []): string
|
||||
{
|
||||
if (preg_match('!vimeo.com\/([0-9]+)!i', $url, $array) === 1) {
|
||||
$id = $array[1];
|
||||
@@ -477,59 +454,106 @@ class Html
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a Youtube video by URL in an iframe
|
||||
* Embeds a YouTube video by URL in an `<iframe>`
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param array $attr
|
||||
* @return string
|
||||
* @param string $url YouTube video URL
|
||||
* @param array $options Query params for the embed URL
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function youtube(string $url, ?array $options = [], array $attr = []): string
|
||||
public static function youtube(string $url, array $options = [], array $attr = []): string
|
||||
{
|
||||
// youtube embed domain
|
||||
$domain = 'youtube.com';
|
||||
$id = null;
|
||||
// default YouTube embed domain
|
||||
$domain = 'youtube.com';
|
||||
$uri = 'embed/';
|
||||
$id = null;
|
||||
$urlOptions = [];
|
||||
|
||||
$schemes = [
|
||||
// http://www.youtube.com/embed/d9NF2edxy-M
|
||||
['pattern' => 'youtube.com\/embed\/([a-zA-Z0-9_-]+)'],
|
||||
// https://www.youtube.com/embed/videoseries?list=PLj8e95eaxiB9goOAvINIy4Vt3mlWQJxys
|
||||
[
|
||||
'pattern' => 'youtube.com\/embed\/videoseries\?list=([a-zA-Z0-9_-]+)',
|
||||
'uri' => 'embed/videoseries?list='
|
||||
],
|
||||
|
||||
// https://www.youtube-nocookie.com/embed/videoseries?list=PLj8e95eaxiB9goOAvINIy4Vt3mlWQJxys
|
||||
[
|
||||
'pattern' => 'youtube-nocookie.com\/embed\/videoseries\?list=([a-zA-Z0-9_-]+)',
|
||||
'domain' => 'www.youtube-nocookie.com',
|
||||
'uri' => 'embed/videoseries?list='
|
||||
],
|
||||
|
||||
// https://www.youtube.com/embed/d9NF2edxy-M
|
||||
// https://www.youtube.com/embed/d9NF2edxy-M?start=10
|
||||
['pattern' => 'youtube.com\/embed\/([a-zA-Z0-9_-]+)(?:\?(.+))?'],
|
||||
|
||||
// https://www.youtube-nocookie.com/embed/d9NF2edxy-M
|
||||
// https://www.youtube-nocookie.com/embed/d9NF2edxy-M?start=10
|
||||
[
|
||||
'pattern' => 'youtube-nocookie.com\/embed\/([a-zA-Z0-9_-]+)',
|
||||
'pattern' => 'youtube-nocookie.com\/embed\/([a-zA-Z0-9_-]+)(?:\?(.+))?',
|
||||
'domain' => 'www.youtube-nocookie.com'
|
||||
],
|
||||
|
||||
// https://www.youtube-nocookie.com/watch?v=d9NF2edxy-M
|
||||
// https://www.youtube-nocookie.com/watch?v=d9NF2edxy-M&t=10
|
||||
[
|
||||
'pattern' => 'youtube-nocookie.com\/watch\?v=([a-zA-Z0-9_-]+)',
|
||||
'pattern' => 'youtube-nocookie.com\/watch\?v=([a-zA-Z0-9_-]+)(?:&(.+))?',
|
||||
'domain' => 'www.youtube-nocookie.com'
|
||||
],
|
||||
// http://www.youtube.com/watch?v=d9NF2edxy-M
|
||||
['pattern' => 'v=([a-zA-Z0-9_-]+)'],
|
||||
// http://youtu.be/d9NF2edxy-M
|
||||
['pattern' => 'youtu.be\/([a-zA-Z0-9_-]+)']
|
||||
|
||||
// https://www.youtube-nocookie.com/playlist?list=PLj8e95eaxiB9goOAvINIy4Vt3mlWQJxys
|
||||
[
|
||||
'pattern' => 'youtube-nocookie.com\/playlist\?list=([a-zA-Z0-9_-]+)',
|
||||
'domain' => 'www.youtube-nocookie.com',
|
||||
'uri' => 'embed/videoseries?list='
|
||||
],
|
||||
|
||||
// https://www.youtube.com/watch?v=d9NF2edxy-M
|
||||
// https://www.youtube.com/watch?v=d9NF2edxy-M&t=10
|
||||
['pattern' => 'youtube.com\/watch\?v=([a-zA-Z0-9_-]+)(?:&(.+))?'],
|
||||
|
||||
// https://www.youtube.com/playlist?list=PLj8e95eaxiB9goOAvINIy4Vt3mlWQJxys
|
||||
[
|
||||
'pattern' => 'youtube.com\/playlist\?list=([a-zA-Z0-9_-]+)',
|
||||
'uri' => 'embed/videoseries?list='
|
||||
],
|
||||
|
||||
// https://youtu.be/d9NF2edxy-M
|
||||
// https://youtu.be/d9NF2edxy-M?t=10
|
||||
['pattern' => 'youtu.be\/([a-zA-Z0-9_-]+)(?:\?(.+))?']
|
||||
];
|
||||
|
||||
foreach ($schemes as $schema) {
|
||||
if (preg_match('!' . $schema['pattern'] . '!i', $url, $array) === 1) {
|
||||
$domain = $schema['domain'] ?? $domain;
|
||||
$uri = $schema['uri'] ?? $uri;
|
||||
$id = $array[1];
|
||||
if (isset($array[2]) === true) {
|
||||
parse_str($array[2], $urlOptions);
|
||||
|
||||
// convert video URL options to embed URL options
|
||||
if (isset($urlOptions['t']) === true) {
|
||||
$urlOptions['start'] = $urlOptions['t'];
|
||||
unset($urlOptions['t']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// no match
|
||||
if ($id === null) {
|
||||
throw new Exception('Invalid Youtube source');
|
||||
throw new Exception('Invalid YouTube source');
|
||||
}
|
||||
|
||||
// build the options query
|
||||
if (empty($options) === false) {
|
||||
$query = '?' . http_build_query($options);
|
||||
if (empty($options) === false || empty($urlOptions) === false) {
|
||||
$query = (Str::contains($uri, '?') === true ? '&' : '?') . http_build_query(array_merge($urlOptions, $options));
|
||||
} else {
|
||||
$query = '';
|
||||
}
|
||||
|
||||
$url = 'https://' . $domain . '/embed/' . $id . $query;
|
||||
$url = 'https://' . $domain . '/' . $uri . $id . $query;
|
||||
|
||||
return static::iframe($url, array_merge(['allowfullscreen' => true], $attr));
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Localization class, roughly inspired by VueI18n
|
||||
@@ -191,7 +190,14 @@ class I18n
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate amounts
|
||||
* Translates amounts
|
||||
*
|
||||
* Translation definition options:
|
||||
* - Translation is a simple string: `{{ count }}` gets replaced in the template
|
||||
* - Translation is an array with a value for each count: Chooses the correct template and
|
||||
* replaces `{{ count }}` in the template; if no specific template for the input count is
|
||||
* defined, the template that is defined last in the translation array is used
|
||||
* - Translation is a callback with a `$count` argument: Returns the callback return value
|
||||
*
|
||||
* @param string $key
|
||||
* @param int $count
|
||||
@@ -206,23 +212,18 @@ class I18n
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_a($translation, 'Closure') === true) {
|
||||
return $translation($count);
|
||||
}
|
||||
|
||||
if (is_string($translation) === true) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
if (count($translation) !== 3) {
|
||||
throw new Exception('Please provide 3 translations');
|
||||
}
|
||||
|
||||
switch ($count) {
|
||||
case 0:
|
||||
$message = $translation[0];
|
||||
break;
|
||||
case 1:
|
||||
$message = $translation[1];
|
||||
break;
|
||||
default:
|
||||
$message = $translation[2];
|
||||
$message = $translation;
|
||||
} else {
|
||||
if (isset($translation[$count]) === true) {
|
||||
$message = $translation[$count];
|
||||
} else {
|
||||
$message = end($translation);
|
||||
}
|
||||
}
|
||||
|
||||
return str_replace('{{ count }}', $count, $message);
|
||||
|
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* The Query class can be used to
|
||||
* query arrays and objects, including their
|
||||
@@ -15,14 +18,13 @@ namespace Kirby\Toolkit;
|
||||
*/
|
||||
class Query
|
||||
{
|
||||
const PARTS = '!([a-zA-Z_][a-zA-Z0-9_]*(\(.*?\))?)\.|' . self::SKIP . '!';
|
||||
const METHOD = '!\((.*)\)!';
|
||||
const PARAMETERS = '!,|' . self::SKIP . '!';
|
||||
const PARTS = '!\.|(\(([^()]+|(?1))*+\))(*SKIP)(*FAIL)!'; // split by dot, but not inside (nested) parens
|
||||
const PARAMETERS = '!,|' . self::SKIP . '!'; // split by comma, but not inside skip groups
|
||||
|
||||
const NO_PNTH = '\([^\(]+\)(*SKIP)(*FAIL)';
|
||||
const NO_PNTH = '\([^(]+\)(*SKIP)(*FAIL)';
|
||||
const NO_SQBR = '\[[^]]+\](*SKIP)(*FAIL)';
|
||||
const NO_DLQU = '\"[^"]+\"(*SKIP)(*FAIL)';
|
||||
const NO_SLQU = '\'[^\']+\'(*SKIP)(*FAIL)';
|
||||
const NO_DLQU = '\"(?:[^"\\\\]|\\\\.)*\"(*SKIP)(*FAIL)'; // allow \" escaping inside string
|
||||
const NO_SLQU = '\'(?:[^\'\\\\]|\\\\.)*\'(*SKIP)(*FAIL)'; // allow \' escaping inside string
|
||||
const SKIP = self::NO_PNTH . '|' . self::NO_SQBR . '|' .
|
||||
self::NO_DLQU . '|' . self::NO_SLQU;
|
||||
|
||||
@@ -43,10 +45,10 @@ class Query
|
||||
/**
|
||||
* Creates a new Query object
|
||||
*
|
||||
* @param string $query
|
||||
* @param string|null $query
|
||||
* @param array|object $data
|
||||
*/
|
||||
public function __construct(string $query = null, $data = [])
|
||||
public function __construct(?string $query = null, $data = [])
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->data = $data;
|
||||
@@ -54,7 +56,7 @@ class Query
|
||||
|
||||
/**
|
||||
* Returns the query result if anything
|
||||
* can be found. Otherwise returns null.
|
||||
* can be found, otherwise returns null
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
@@ -69,10 +71,12 @@ class Query
|
||||
|
||||
/**
|
||||
* Resolves the query if anything
|
||||
* can be found. Otherwise returns null.
|
||||
* can be found, otherwise returns null
|
||||
*
|
||||
* @param string $query
|
||||
* @return mixed
|
||||
*
|
||||
* @throws Kirby\Exception\BadMethodCallException If an invalid method is accessed by the query
|
||||
*/
|
||||
protected function resolve(string $query)
|
||||
{
|
||||
@@ -85,27 +89,47 @@ class Query
|
||||
$data = $this->data;
|
||||
$value = null;
|
||||
|
||||
while (count($parts)) {
|
||||
$part = array_shift($parts);
|
||||
foreach ($parts as $part) {
|
||||
$info = $this->part($part);
|
||||
$method = $info['method'];
|
||||
$value = null;
|
||||
$args = $info['args'];
|
||||
|
||||
if (is_array($data)) {
|
||||
$value = $data[$method] ?? null;
|
||||
} elseif (is_object($data)) {
|
||||
if (method_exists($data, $method) || method_exists($data, '__call')) {
|
||||
$value = $data->$method(...$info['args']);
|
||||
if (array_key_exists($method, $data) === true) {
|
||||
$value = $data[$method];
|
||||
|
||||
if (is_a($value, 'Closure') === true) {
|
||||
$value = $value(...$args);
|
||||
} elseif ($args !== []) {
|
||||
throw new InvalidArgumentException('Cannot access array element ' . $method . ' with arguments');
|
||||
}
|
||||
} else {
|
||||
static::accessError($data, $method, 'property');
|
||||
}
|
||||
} elseif (is_object($data)) {
|
||||
if (
|
||||
method_exists($data, $method) === true ||
|
||||
method_exists($data, '__call') === true
|
||||
) {
|
||||
$value = $data->$method(...$args);
|
||||
} elseif (
|
||||
$args === [] && (
|
||||
property_exists($data, $method) === true ||
|
||||
method_exists($data, '__get') === true
|
||||
)
|
||||
) {
|
||||
$value = $data->$method;
|
||||
} else {
|
||||
$label = ($args === []) ? 'method/property' : 'method';
|
||||
static::accessError($data, $method, $label);
|
||||
}
|
||||
} elseif (is_scalar($data)) {
|
||||
return $data;
|
||||
} else {
|
||||
return null;
|
||||
// further parts on a scalar/null value
|
||||
static::accessError($data, $method, 'method/property');
|
||||
}
|
||||
|
||||
if (is_array($value) || is_object($value)) {
|
||||
$data = $value;
|
||||
}
|
||||
// continue with the current value for the next part
|
||||
$data = $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
@@ -119,60 +143,54 @@ class Query
|
||||
*/
|
||||
protected function parts(string $query): array
|
||||
{
|
||||
$query = trim($query);
|
||||
|
||||
// match all parts but the last
|
||||
preg_match_all(self::PARTS, $query, $match);
|
||||
|
||||
// remove all matched parts from the query to retrieve last part
|
||||
foreach ($match[0] as $part) {
|
||||
$query = Str::after($query, $part);
|
||||
}
|
||||
|
||||
array_push($match[1], $query);
|
||||
return $match[1];
|
||||
return preg_split(self::PARTS, trim($query), -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes each part of the query string and
|
||||
* extracts methods and method arguments.
|
||||
* extracts methods and method arguments
|
||||
*
|
||||
* @param string $part
|
||||
* @return array
|
||||
*/
|
||||
protected function part(string $part): array
|
||||
{
|
||||
$args = [];
|
||||
$method = preg_replace_callback(self::METHOD, function ($match) use (&$args) {
|
||||
$args = preg_split(self::PARAMETERS, $match[1]);
|
||||
$args = array_map('self::parameter', $args);
|
||||
}, $part);
|
||||
if (Str::endsWith($part, ')') === true) {
|
||||
$method = Str::before($part, '(');
|
||||
|
||||
return [
|
||||
'method' => $method,
|
||||
'args' => $args
|
||||
];
|
||||
// the args are everything inside the *outer* parentheses
|
||||
$args = Str::substr($part, Str::position($part, '(') + 1, -1);
|
||||
$args = preg_split(self::PARAMETERS, $args);
|
||||
$args = array_map('self::parameter', $args);
|
||||
|
||||
return compact('method', 'args');
|
||||
} else {
|
||||
return [
|
||||
'method' => $part,
|
||||
'args' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parameter of query to
|
||||
* proper type.
|
||||
* Converts a parameter of a query to
|
||||
* its proper native PHP type
|
||||
*
|
||||
* @param mixed $arg
|
||||
* @param string $arg
|
||||
* @return mixed
|
||||
*/
|
||||
protected function parameter($arg)
|
||||
protected function parameter(string $arg)
|
||||
{
|
||||
$arg = trim($arg);
|
||||
|
||||
// string with double quotes
|
||||
if (substr($arg, 0, 1) === '"') {
|
||||
return trim($arg, '"');
|
||||
if (substr($arg, 0, 1) === '"' && substr($arg, -1) === '"') {
|
||||
return str_replace('\"', '"', substr($arg, 1, -1));
|
||||
}
|
||||
|
||||
// string with single quotes
|
||||
if (substr($arg, 0, 1) === '\'') {
|
||||
return trim($arg, '\'');
|
||||
if (substr($arg, 0, 1) === "'" && substr($arg, -1) === "'") {
|
||||
return str_replace("\'", "'", substr($arg, 1, -1));
|
||||
}
|
||||
|
||||
// boolean or null
|
||||
@@ -200,4 +218,27 @@ class Query
|
||||
// resolve parameter for objects and methods itself
|
||||
return $this->resolve($arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception for an access to an invalid method
|
||||
*
|
||||
* @param mixed $data Variable on which the access was tried
|
||||
* @param string $name Name of the method/property that was accessed
|
||||
* @param string $label Type of the name (`method`, `property` or `method/property`)
|
||||
* @return void
|
||||
*
|
||||
* @throws Kirby\Exception\BadMethodCallException
|
||||
*/
|
||||
protected static function accessError($data, string $name, string $label): void
|
||||
{
|
||||
$type = strtolower(gettype($data));
|
||||
if ($type === 'double') {
|
||||
$type = 'float';
|
||||
}
|
||||
|
||||
$nonExisting = in_array($type, ['array', 'object']) ? 'non-existing ' : '';
|
||||
|
||||
$error = 'Access to ' . $nonExisting . $label . ' ' . $name . ' on ' . $type;
|
||||
throw new BadMethodCallException($error);
|
||||
}
|
||||
}
|
||||
|
@@ -341,7 +341,7 @@ class Str
|
||||
* @param string $rep The element, which should be added if the string is too long. Ellipsis is the default.
|
||||
* @return string The shortened string
|
||||
*/
|
||||
public static function excerpt($string, $chars = 140, $strip = true, $rep = '…')
|
||||
public static function excerpt($string, $chars = 140, $strip = true, $rep = ' …')
|
||||
{
|
||||
if ($strip === true) {
|
||||
$string = strip_tags(str_replace('<', ' <', $string));
|
||||
@@ -361,7 +361,7 @@ class Str
|
||||
return $string;
|
||||
}
|
||||
|
||||
return static::substr($string, 0, mb_strrpos(static::substr($string, 0, $chars), ' ')) . ' ' . $rep;
|
||||
return static::substr($string, 0, mb_strrpos(static::substr($string, 0, $chars), ' ')) . $rep;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -897,10 +897,25 @@ class Str
|
||||
{
|
||||
return preg_replace_callback('!' . $start . '(.*?)' . $end . '!', function ($match) use ($data, $fallback) {
|
||||
$query = trim($match[1]);
|
||||
|
||||
// if the placeholder contains a dot, it is a query
|
||||
if (strpos($query, '.') !== false) {
|
||||
return (new Query($match[1], $data))->result() ?? $fallback;
|
||||
try {
|
||||
$result = (new Query($match[1], $data))->result();
|
||||
} catch (Exception $e) {
|
||||
$result = null;
|
||||
}
|
||||
} else {
|
||||
$result = $data[$query] ?? null;
|
||||
}
|
||||
return $data[$query] ?? $fallback;
|
||||
|
||||
// if we don't have a result, use the fallback if given
|
||||
if ($result === null && $fallback !== null) {
|
||||
$result = $fallback;
|
||||
}
|
||||
|
||||
// if we still don't have a result, keep the original placeholder
|
||||
return $result ?? $match[0];
|
||||
}, $string);
|
||||
}
|
||||
|
||||
|
@@ -18,23 +18,21 @@ class Tpl
|
||||
/**
|
||||
* Renders the template
|
||||
*
|
||||
* @param string $__file
|
||||
* @param array $__data
|
||||
* @param string $file
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public static function load(string $__file = null, array $__data = []): string
|
||||
public static function load(string $file = null, array $data = []): string
|
||||
{
|
||||
if (file_exists($__file) === false) {
|
||||
if (is_file($file) === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$exception = null;
|
||||
|
||||
ob_start();
|
||||
extract($__data);
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
require $__file;
|
||||
F::load($file, null, $data);
|
||||
} catch (Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Http\Idn;
|
||||
use ReflectionFunction;
|
||||
use Throwable;
|
||||
@@ -239,13 +240,47 @@ V::$validators = [
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid date
|
||||
* Checks for a valid date or compares two
|
||||
* dates with each other.
|
||||
*
|
||||
* Pass only the first argument to check for a valid date.
|
||||
* Pass an operator as second argument and another date as
|
||||
* third argument to compare them.
|
||||
*/
|
||||
'date' => function ($value): bool {
|
||||
$date = date_parse($value);
|
||||
return $date !== false &&
|
||||
$date['error_count'] === 0 &&
|
||||
$date['warning_count'] === 0;
|
||||
'date' => function (?string $value, string $operator = null, string $test = null): bool {
|
||||
$args = func_get_args();
|
||||
|
||||
// simple date validation
|
||||
if (count($args) === 1) {
|
||||
$date = date_parse($value);
|
||||
return $date !== false &&
|
||||
$date['error_count'] === 0 &&
|
||||
$date['warning_count'] === 0;
|
||||
}
|
||||
|
||||
$value = strtotime($value);
|
||||
$test = strtotime($test);
|
||||
|
||||
if (is_int($value) !== true || is_int($test) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($operator) {
|
||||
case '!=':
|
||||
return $value !== $test;
|
||||
case '<':
|
||||
return $value < $test;
|
||||
case '>':
|
||||
return $value > $test;
|
||||
case '<=':
|
||||
return $value <= $test;
|
||||
case '>=':
|
||||
return $value >= $test;
|
||||
case '==':
|
||||
return $value === $test;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Invalid date comparison operator: "' . $operator . '". Allowed operators: "==", "!=", "<", "<=", ">", ">="');
|
||||
},
|
||||
|
||||
/**
|
||||
|
@@ -60,7 +60,7 @@ class View
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->file()) === true;
|
||||
return is_file($this->file()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,13 +94,11 @@ class View
|
||||
throw new Exception($this->missingViewMessage());
|
||||
}
|
||||
|
||||
$exception = null;
|
||||
|
||||
ob_start();
|
||||
extract($this->data());
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
require $this->file();
|
||||
F::load($this->file(), null, $this->data());
|
||||
} catch (Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
@@ -2,8 +2,10 @@
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use SimpleXMLElement;
|
||||
|
||||
/**
|
||||
* XML parser and creator Class
|
||||
* XML parser and creator class
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
@@ -14,7 +16,7 @@ namespace Kirby\Toolkit;
|
||||
class Xml
|
||||
{
|
||||
/**
|
||||
* Conversion table for html entities
|
||||
* HTML to XML conversion table for entities
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
@@ -54,166 +56,349 @@ class Xml
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates an XML string from an array
|
||||
* Closing string for void tags
|
||||
*
|
||||
* @param string $props The source array
|
||||
* @param string $name The name of the root element
|
||||
* @param bool $head Include the xml declaration head or not
|
||||
* @param int $level The indendation level
|
||||
* @return string The XML string
|
||||
* @var string
|
||||
*/
|
||||
public static function create($props, string $name = 'root', bool $head = true, $level = 0): string
|
||||
public static $void = ' />';
|
||||
|
||||
/**
|
||||
* Generates a single attribute or a list of attributes
|
||||
*
|
||||
* @param string|array $name String: A single attribute with that name will be generated.
|
||||
* Key-value array: A list of attributes will be generated. Don't pass a second argument in that case.
|
||||
* @param mixed $value If used with a `$name` string, pass the value of the attribute here.
|
||||
* If used with a `$name` array, this can be set to `false` to disable attribute sorting.
|
||||
* @return string|null The generated XML attributes string
|
||||
*/
|
||||
public static function attr($name, $value = null): ?string
|
||||
{
|
||||
$attributes = $props['@attributes'] ?? null;
|
||||
$value = $props['@value'] ?? null;
|
||||
$children = $props;
|
||||
$indent = str_repeat(' ', $level);
|
||||
$nextLevel = $level + 1;
|
||||
if (is_array($name) === true) {
|
||||
if ($value !== false) {
|
||||
ksort($name);
|
||||
}
|
||||
|
||||
if (is_array($children) === true) {
|
||||
unset($children['@attributes'], $children['@value']);
|
||||
$attributes = [];
|
||||
foreach ($name as $key => $val) {
|
||||
$a = static::attr($key, $val);
|
||||
|
||||
$childTags = [];
|
||||
|
||||
foreach ($children as $childName => $childItems) {
|
||||
if (is_array($childItems) === true) {
|
||||
|
||||
// another tag with attributes
|
||||
if (A::isAssociative($childItems) === true) {
|
||||
$childTags[] = static::create($childItems, $childName, false, $level);
|
||||
|
||||
// just children
|
||||
} else {
|
||||
foreach ($childItems as $childItem) {
|
||||
$childTags[] = static::create($childItem, $childName, false, $nextLevel);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$childTags[] = static::tag($childName, $childItems, null, $indent);
|
||||
if ($a) {
|
||||
$attributes[] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($childTags) === false) {
|
||||
$value = $childTags;
|
||||
}
|
||||
return implode(' ', $attributes);
|
||||
}
|
||||
|
||||
$result = $head === true ? '<?xml version="1.0" encoding="utf-8"?>' . PHP_EOL : null;
|
||||
$result .= static::tag($name, $value, $attributes, $indent);
|
||||
if ($value === null || $value === '' || $value === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
if ($value === ' ') {
|
||||
return strtolower($name) . '=""';
|
||||
}
|
||||
|
||||
if (is_bool($value) === true) {
|
||||
return $value === true ? strtolower($name) . '="' . strtolower($name) . '"' : null;
|
||||
}
|
||||
|
||||
if (is_array($value) === true) {
|
||||
if (isset($value['value'], $value['escape'])) {
|
||||
$value = $value['escape'] === true ? static::encode($value['value']) : $value['value'];
|
||||
} else {
|
||||
$value = implode(' ', array_filter($value, function ($value) {
|
||||
return !empty($value) || is_numeric($value);
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
$value = static::encode($value);
|
||||
}
|
||||
|
||||
return strtolower($name) . '="' . $value . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all xml entities from a string
|
||||
* and convert them to html entities first
|
||||
* and remove all html entities afterwards.
|
||||
* Creates an XML string from an array
|
||||
*
|
||||
* <code>
|
||||
* Supports special array keys `@name` (element name),
|
||||
* `@attributes` (XML attribute key-value array),
|
||||
* `@namespaces` (array with XML namespaces) and
|
||||
* `@value` (element content)
|
||||
*
|
||||
* echo xml::decode('some <em>über</em> crazy stuff');
|
||||
* // output: some über crazy stuff
|
||||
* @param array|string $props The source array or tag content (used internally)
|
||||
* @param string $name The name of the root element
|
||||
* @param bool $head Include the XML declaration head or not
|
||||
* @param string $indent Indentation string, defaults to two spaces
|
||||
* @param int $level The indendation level (used internally)
|
||||
* @return string The XML string
|
||||
*/
|
||||
public static function create($props, string $name = 'root', bool $head = true, string $indent = ' ', int $level = 0): string
|
||||
{
|
||||
if (is_array($props) === true) {
|
||||
if (A::isAssociative($props) === true) {
|
||||
// a tag with attributes or named children
|
||||
|
||||
// extract metadata from special array keys
|
||||
$name = $props['@name'] ?? $name;
|
||||
$attributes = $props['@attributes'] ?? [];
|
||||
$value = $props['@value'] ?? null;
|
||||
if (isset($props['@namespaces'])) {
|
||||
foreach ($props['@namespaces'] as $key => $namespace) {
|
||||
$key = 'xmlns' . (($key)? ':' . $key : '');
|
||||
$attributes[$key] = $namespace;
|
||||
}
|
||||
}
|
||||
|
||||
// continue with just the children
|
||||
unset($props['@name'], $props['@attributes'], $props['@namespaces'], $props['@value']);
|
||||
|
||||
if (count($props) > 0) {
|
||||
// there are children, use them instead of the value
|
||||
|
||||
$value = [];
|
||||
foreach ($props as $childName => $childItem) {
|
||||
// render the child, but don't include the indentation of the first line
|
||||
$value[] = trim(static::create($childItem, $childName, false, $indent, $level + 1));
|
||||
}
|
||||
}
|
||||
|
||||
$result = static::tag($name, $value, $attributes, $indent, $level);
|
||||
} else {
|
||||
// just children
|
||||
|
||||
$result = [];
|
||||
foreach ($props as $childItem) {
|
||||
$result[] = static::create($childItem, $name, false, $indent, $level);
|
||||
}
|
||||
|
||||
$result = implode(PHP_EOL, $result);
|
||||
}
|
||||
} else {
|
||||
// scalar value
|
||||
|
||||
$result = static::tag($name, $props, null, $indent, $level);
|
||||
}
|
||||
|
||||
if ($head === true) {
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL . $result;
|
||||
} else {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all HTML/XML tags and encoded chars from a string
|
||||
*
|
||||
* </code>
|
||||
* ```
|
||||
* echo Xml::decode('some über <em>crazy</em> stuff');
|
||||
* // output: some über crazy stuff
|
||||
* ```
|
||||
*
|
||||
* @param string $string
|
||||
* @param string|null $string
|
||||
* @return string
|
||||
*/
|
||||
public static function decode(string $string = null): string
|
||||
public static function decode(?string $string): string
|
||||
{
|
||||
return Html::decode($string);
|
||||
if ($string === null) {
|
||||
$string = '';
|
||||
}
|
||||
|
||||
$string = strip_tags($string);
|
||||
return html_entity_decode($string, ENT_COMPAT, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to a xml-safe string
|
||||
* Converts it to html-safe first and then it
|
||||
* will replace html entities to xml entities
|
||||
* Converts a string to an XML-safe string
|
||||
*
|
||||
* <code>
|
||||
* Converts it to HTML-safe first and then it
|
||||
* will replace HTML entities with XML entities
|
||||
*
|
||||
* echo xml::encode('some über crazy stuff');
|
||||
* ```php
|
||||
* echo Xml::encode('some über crazy stuff');
|
||||
* // output: some über crazy stuff
|
||||
* ```
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string
|
||||
* @param bool $html True: convert to html first
|
||||
* @param string|null $string
|
||||
* @param bool $html True = Convert to HTML-safe first
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(string $string = null, bool $html = true): string
|
||||
public static function encode(?string $string, bool $html = true): string
|
||||
{
|
||||
if ($string === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($html === true) {
|
||||
$string = Html::encode($string, false);
|
||||
}
|
||||
|
||||
$entities = static::entities();
|
||||
$searches = array_keys($entities);
|
||||
$values = array_values($entities);
|
||||
$entities = self::entities();
|
||||
$html = array_keys($entities);
|
||||
$xml = array_values($entities);
|
||||
|
||||
return str_replace($searches, $values, $string);
|
||||
return str_replace($html, $xml, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the html to xml entities translation table
|
||||
* Returns the HTML-to-XML entity translation table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function entities(): array
|
||||
{
|
||||
return static::$entities;
|
||||
return self::$entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a XML string and returns an array
|
||||
* Parses an XML string and returns an array
|
||||
*
|
||||
* @param string $xml
|
||||
* @return array|false
|
||||
* @return array|null Parsed array or `null` on error
|
||||
*/
|
||||
public static function parse(string $xml = null)
|
||||
public static function parse(string $xml): ?array
|
||||
{
|
||||
$xml = preg_replace('/(<\/?)(\w+):([^>]*>)/', '$1$2$3', $xml);
|
||||
$xml = @simplexml_load_string($xml, null, LIBXML_NOENT | LIBXML_NOCDATA);
|
||||
$xml = @simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);
|
||||
|
||||
$xml = @json_encode($xml);
|
||||
$xml = @json_decode($xml, true);
|
||||
return is_array($xml) === true ? $xml : false;
|
||||
if (is_object($xml) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::simplify($xml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks a SimpleXMLElement down into a simpler tree
|
||||
* structure of arrays and strings
|
||||
*
|
||||
* @param \SimpleXMLElement $element
|
||||
* @param bool $collectName Whether the element name should be collected (for the root element)
|
||||
* @return array|string
|
||||
*/
|
||||
public static function simplify(SimpleXMLElement $element, bool $collectName = true)
|
||||
{
|
||||
// get all XML namespaces of the whole document to iterate over later;
|
||||
// we don't need the global namespace (empty string) in the list
|
||||
$usedNamespaces = $element->getNamespaces(true);
|
||||
if (isset($usedNamespaces[''])) {
|
||||
unset($usedNamespaces['']);
|
||||
}
|
||||
|
||||
// now collect element metadata of the parent
|
||||
$array = [];
|
||||
if ($collectName === true) {
|
||||
$array['@name'] = $element->getName();
|
||||
}
|
||||
|
||||
// collect attributes with each defined document namespace;
|
||||
// also check for attributes without any namespace
|
||||
$attributeArray = [];
|
||||
foreach (array_merge([0 => null], array_keys($usedNamespaces)) as $namespace) {
|
||||
$prefix = ($namespace)? $namespace . ':' : '';
|
||||
$attributes = $element->attributes($namespace, true);
|
||||
|
||||
foreach ($attributes as $key => $value) {
|
||||
$attributeArray[$prefix . $key] = (string)$value;
|
||||
}
|
||||
}
|
||||
if (count($attributeArray) > 0) {
|
||||
$array['@attributes'] = $attributeArray;
|
||||
}
|
||||
|
||||
// collect namespace definitions of this particular XML element
|
||||
if ($namespaces = $element->getDocNamespaces(false, false)) {
|
||||
$array['@namespaces'] = $namespaces;
|
||||
}
|
||||
|
||||
// check for children with each defined document namespace;
|
||||
// also check for children without any namespace
|
||||
$hasChildren = false;
|
||||
foreach (array_merge([0 => null], array_keys($usedNamespaces)) as $namespace) {
|
||||
$prefix = ($namespace)? $namespace . ':' : '';
|
||||
$children = $element->children($namespace, true);
|
||||
|
||||
if (count($children) > 0) {
|
||||
// there are children, recursively simplify each one
|
||||
$hasChildren = true;
|
||||
|
||||
// make a grouped collection of elements per element name
|
||||
foreach ($children as $child) {
|
||||
$array[$prefix . $child->getName()][] = static::simplify($child, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($hasChildren === true) {
|
||||
// there were children of any namespace
|
||||
|
||||
// reduce elements where there is only one item
|
||||
// of the respective type to a simple string;
|
||||
// don't do anything with special `@` metadata keys
|
||||
foreach ($array as $name => $item) {
|
||||
if (substr($name, 0, 1) !== '@' && count($item) === 1) {
|
||||
$array[$name] = $item[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
} else {
|
||||
// we didn't find any XML children above, only use the string value
|
||||
$element = (string)$element;
|
||||
|
||||
if (count($array) > 0) {
|
||||
$array['@value'] = $element;
|
||||
|
||||
return $array;
|
||||
} else {
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an XML tag
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $content
|
||||
* @param array $attr
|
||||
* @param mixed $indent
|
||||
* @return string
|
||||
* @param string $name Tag name
|
||||
* @param array|string|null $content Scalar value or array with multiple lines of content or `null` to
|
||||
* generate a self-closing tag; pass an empty string to generate empty content
|
||||
* @param array $attr An associative array with additional attributes for the tag
|
||||
* @param string|null $indent Indentation string, defaults to two spaces or `null` for output on one line
|
||||
* @param int $level Indentation level
|
||||
* @return string The generated XML
|
||||
*/
|
||||
public static function tag(string $name, $content = null, array $attr = null, $indent = null): string
|
||||
public static function tag(string $name, $content = '', array $attr = null, ?string $indent = null, int $level = 0): string
|
||||
{
|
||||
$attr = Html::attr($attr);
|
||||
$start = '<' . $name . ($attr ? ' ' . $attr : null) . '>';
|
||||
$end = '</' . $name . '>';
|
||||
$attr = static::attr($attr);
|
||||
$start = '<' . $name . ($attr ? ' ' . $attr : '') . '>';
|
||||
$startShort = '<' . $name . ($attr ? ' ' . $attr : '') . static::$void;
|
||||
$end = '</' . $name . '>';
|
||||
$baseIndent = $indent ? str_repeat($indent, $level) : '';
|
||||
|
||||
if (is_array($content) === true) {
|
||||
$xml = $indent . $start . PHP_EOL;
|
||||
foreach ($content as $line) {
|
||||
$xml .= $indent . $indent . $line . PHP_EOL;
|
||||
if (is_string($indent) === true) {
|
||||
$xml = $baseIndent . $start . PHP_EOL;
|
||||
foreach ($content as $line) {
|
||||
$xml .= $baseIndent . $indent . $line . PHP_EOL;
|
||||
}
|
||||
$xml .= $baseIndent . $end;
|
||||
} else {
|
||||
$xml = $start . implode($content) . $end;
|
||||
}
|
||||
$xml .= $indent . $end;
|
||||
} elseif ($content === null) {
|
||||
$xml = $baseIndent . $startShort;
|
||||
} else {
|
||||
$xml = $indent . $start . static::value($content) . $end;
|
||||
$xml = $baseIndent . $start . static::value($content) . $end;
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the value as cdata if necessary
|
||||
* Properly encodes tag contents
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
* @return string|null
|
||||
*/
|
||||
public static function value($value)
|
||||
public static function value($value): ?string
|
||||
{
|
||||
if ($value === true) {
|
||||
return 'true';
|
||||
@@ -224,23 +409,25 @@ class Xml
|
||||
}
|
||||
|
||||
if (is_numeric($value) === true) {
|
||||
return $value;
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Str::contains($value, '<![CDATA[') === true) {
|
||||
if (Str::startsWith($value, '<![CDATA[') === true) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$encoded = htmlentities($value);
|
||||
|
||||
if ($encoded === $value) {
|
||||
// no CDATA block needed
|
||||
return $value;
|
||||
}
|
||||
|
||||
return '<![CDATA[' . static::encode($value) . ']]>';
|
||||
// wrap everything in a CDATA block
|
||||
// and ensure that it is not closed in the input string
|
||||
return '<![CDATA[' . str_replace(']]>', ']]]]><![CDATA[>', $value) . ']]>';
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user