Update Composer packages
This commit is contained in:
parent
0320235f6c
commit
a8b68fb61b
378 changed files with 28466 additions and 28852 deletions
|
@ -4,10 +4,14 @@ namespace Kirby\Api;
|
|||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Exception\Exception as ExceptionException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Http\Route;
|
||||
use Kirby\Http\Router;
|
||||
use Kirby\Toolkit\Collection as BaseCollection;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Pagination;
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
@ -32,82 +36,59 @@ class Api
|
|||
|
||||
/**
|
||||
* Authentication callback
|
||||
*
|
||||
* @var \Closure
|
||||
*/
|
||||
protected $authentication;
|
||||
protected Closure|null $authentication = null;
|
||||
|
||||
/**
|
||||
* Debugging flag
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $debug = false;
|
||||
protected bool $debug = false;
|
||||
|
||||
/**
|
||||
* Collection definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $collections = [];
|
||||
protected array $collections = [];
|
||||
|
||||
/**
|
||||
* Injected data/dependencies
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* Model definitions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $models = [];
|
||||
protected array $models = [];
|
||||
|
||||
/**
|
||||
* The current route
|
||||
*
|
||||
* @var \Kirby\Http\Route
|
||||
*/
|
||||
protected $route;
|
||||
protected Route|null $route = null;
|
||||
|
||||
/**
|
||||
* The Router instance
|
||||
*
|
||||
* @var \Kirby\Http\Router
|
||||
*/
|
||||
protected $router;
|
||||
protected Router|null $router = null;
|
||||
|
||||
/**
|
||||
* Route definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $routes = [];
|
||||
protected array $routes = [];
|
||||
|
||||
/**
|
||||
* Request data
|
||||
* [query, body, files]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $requestData = [];
|
||||
protected array $requestData = [];
|
||||
|
||||
/**
|
||||
* The applied request method
|
||||
* (GET, POST, PATCH, etc.)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $requestMethod;
|
||||
protected string|null $requestMethod = null;
|
||||
|
||||
/**
|
||||
* Magic accessor for any given data
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
* @throws \Kirby\Exception\NotFoundException
|
||||
*/
|
||||
public function __call(string $method, array $args = [])
|
||||
|
@ -117,8 +98,6 @@ class Api
|
|||
|
||||
/**
|
||||
* Creates a new API instance
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
|
@ -128,16 +107,10 @@ class Api
|
|||
/**
|
||||
* Runs the authentication method
|
||||
* if set
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
if ($auth = $this->authentication()) {
|
||||
return $auth->call($this);
|
||||
}
|
||||
|
||||
return true;
|
||||
return $this->authentication()?->call($this) ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -145,7 +118,7 @@ class Api
|
|||
*
|
||||
* @return \Closure|null
|
||||
*/
|
||||
public function authentication()
|
||||
public function authentication(): Closure|null
|
||||
{
|
||||
return $this->authentication;
|
||||
}
|
||||
|
@ -154,14 +127,10 @@ class Api
|
|||
* Execute an API call for the given path,
|
||||
* request method and optional request data
|
||||
*
|
||||
* @param string|null $path
|
||||
* @param string $method
|
||||
* @param array $requestData
|
||||
* @return mixed
|
||||
* @throws \Kirby\Exception\NotFoundException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function call(string $path = null, string $method = 'GET', array $requestData = [])
|
||||
public function call(string|null $path = null, string $method = 'GET', array $requestData = [])
|
||||
{
|
||||
$path = rtrim($path ?? '', '/');
|
||||
|
||||
|
@ -170,19 +139,18 @@ class Api
|
|||
|
||||
$this->router = new Router($this->routes());
|
||||
$this->route = $this->router->find($path, $method);
|
||||
$auth = $this->route->attributes()['auth'] ?? true;
|
||||
$auth = $this->route?->attributes()['auth'] ?? true;
|
||||
|
||||
if ($auth !== false) {
|
||||
$user = $this->authenticate();
|
||||
|
||||
// set PHP locales based on *user* language
|
||||
// so that e.g. strftime() gets formatted correctly
|
||||
if (is_a($user, 'Kirby\Cms\User') === true) {
|
||||
if ($user instanceof User) {
|
||||
$language = $user->language();
|
||||
|
||||
// get the locale from the translation
|
||||
$translation = $user->kirby()->translation($language);
|
||||
$locale = ($translation !== null) ? $translation->locale() : $language;
|
||||
$locale = $user->kirby()->translation($language)->locale();
|
||||
|
||||
// provide some variants as fallbacks to be
|
||||
// compatible with as many systems as possible
|
||||
|
@ -208,14 +176,17 @@ class Api
|
|||
$validate = Pagination::$validate;
|
||||
Pagination::$validate = false;
|
||||
|
||||
$output = $this->route->action()->call($this, ...$this->route->arguments());
|
||||
$output = $this->route?->action()->call(
|
||||
$this,
|
||||
...$this->route->arguments()
|
||||
);
|
||||
|
||||
// restore old pagination validation mode
|
||||
Pagination::$validate = $validate;
|
||||
|
||||
if (
|
||||
is_object($output) === true &&
|
||||
is_a($output, 'Kirby\\Http\\Response') !== true
|
||||
$output instanceof Response === false
|
||||
) {
|
||||
return $this->resolve($output)->toResponse();
|
||||
}
|
||||
|
@ -226,13 +197,10 @@ class Api
|
|||
/**
|
||||
* Setter and getter for an API collection
|
||||
*
|
||||
* @param string $name
|
||||
* @param array|null $collection
|
||||
* @return \Kirby\Api\Collection
|
||||
* @throws \Kirby\Exception\NotFoundException If no collection for `$name` exists
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function collection(string $name, $collection = null)
|
||||
public function collection(string $name, array|BaseCollection|null $collection = null): Collection
|
||||
{
|
||||
if (isset($this->collections[$name]) === false) {
|
||||
throw new NotFoundException(sprintf('The collection "%s" does not exist', $name));
|
||||
|
@ -243,8 +211,6 @@ class Api
|
|||
|
||||
/**
|
||||
* Returns the collections definition
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function collections(): array
|
||||
{
|
||||
|
@ -255,13 +221,9 @@ class Api
|
|||
* Returns the injected data array
|
||||
* or certain parts of it by key
|
||||
*
|
||||
* @param string|null $key
|
||||
* @param mixed ...$args
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If no data for `$key` exists
|
||||
*/
|
||||
public function data($key = null, ...$args)
|
||||
public function data(string|null $key = null, ...$args)
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->data;
|
||||
|
@ -272,7 +234,7 @@ class Api
|
|||
}
|
||||
|
||||
// lazy-load data wrapped in Closures
|
||||
if (is_a($this->data[$key], 'Closure') === true) {
|
||||
if ($this->data[$key] instanceof Closure) {
|
||||
return $this->data[$key]->call($this, ...$args);
|
||||
}
|
||||
|
||||
|
@ -281,8 +243,6 @@ class Api
|
|||
|
||||
/**
|
||||
* Returns the debugging flag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function debug(): bool
|
||||
{
|
||||
|
@ -291,9 +251,6 @@ class Api
|
|||
|
||||
/**
|
||||
* Checks if injected data exists for the given key
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function hasData(string $key): bool
|
||||
{
|
||||
|
@ -305,39 +262,31 @@ class Api
|
|||
* based on the `type` field
|
||||
*
|
||||
* @param array models or collections
|
||||
* @param mixed $object
|
||||
*
|
||||
* @return string key of match
|
||||
* @return string|null key of match
|
||||
*/
|
||||
protected function match(array $array, $object = null)
|
||||
protected function match(array $array, $object = null): string|null
|
||||
{
|
||||
foreach ($array as $definition => $model) {
|
||||
if (is_a($object, $model['type']) === true) {
|
||||
if ($object instanceof $model['type']) {
|
||||
return $definition;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an API model instance by name
|
||||
*
|
||||
* @param string|null $name
|
||||
* @param mixed $object
|
||||
* @return \Kirby\Api\Model
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If no model for `$name` exists
|
||||
*/
|
||||
public function model(string $name = null, $object = null)
|
||||
public function model(string|null $name = null, $object = null): Model
|
||||
{
|
||||
// Try to auto-match object with API models
|
||||
if ($name === null) {
|
||||
if ($model = $this->match($this->models, $object)) {
|
||||
$name = $model;
|
||||
}
|
||||
}
|
||||
$name ??= $this->match($this->models, $object);
|
||||
|
||||
if (isset($this->models[$name]) === false) {
|
||||
throw new NotFoundException(sprintf('The model "%s" does not exist', $name));
|
||||
throw new NotFoundException(sprintf('The model "%s" does not exist', $name ?? 'NULL'));
|
||||
}
|
||||
|
||||
return new Model($this, $object, $this->models[$name]);
|
||||
|
@ -345,8 +294,6 @@ class Api
|
|||
|
||||
/**
|
||||
* Returns all model definitions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function models(): array
|
||||
{
|
||||
|
@ -363,8 +310,11 @@ class Api
|
|||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestData(string $type = null, string $key = null, $default = null)
|
||||
{
|
||||
public function requestData(
|
||||
string|null $type = null,
|
||||
string|null $key = null,
|
||||
$default = null
|
||||
) {
|
||||
if ($type === null) {
|
||||
return $this->requestData;
|
||||
}
|
||||
|
@ -381,58 +331,40 @@ class Api
|
|||
|
||||
/**
|
||||
* Returns the request body if available
|
||||
*
|
||||
* @param string|null $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestBody(string $key = null, $default = null)
|
||||
public function requestBody(string|null $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('body', $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the files from the request if available
|
||||
*
|
||||
* @param string|null $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestFiles(string $key = null, $default = null)
|
||||
public function requestFiles(string|null $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('files', $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all headers from the request if available
|
||||
*
|
||||
* @param string|null $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestHeaders(string $key = null, $default = null)
|
||||
public function requestHeaders(string|null $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('headers', $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request method
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function requestMethod(): string
|
||||
public function requestMethod(): string|null
|
||||
{
|
||||
return $this->requestMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request query if available
|
||||
*
|
||||
* @param string|null $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestQuery(string $key = null, $default = null)
|
||||
public function requestQuery(string|null $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('query', $key, $default);
|
||||
}
|
||||
|
@ -441,14 +373,14 @@ class Api
|
|||
* Turns a Kirby object into an
|
||||
* API model or collection representation
|
||||
*
|
||||
* @param mixed $object
|
||||
* @return \Kirby\Api\Model|\Kirby\Api\Collection
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If `$object` cannot be resolved
|
||||
*/
|
||||
public function resolve($object)
|
||||
public function resolve($object): Model|Collection
|
||||
{
|
||||
if (is_a($object, 'Kirby\Api\Model') === true || is_a($object, 'Kirby\Api\Collection') === true) {
|
||||
if (
|
||||
$object instanceof Model ||
|
||||
$object instanceof Collection
|
||||
) {
|
||||
return $object;
|
||||
}
|
||||
|
||||
|
@ -465,8 +397,6 @@ class Api
|
|||
|
||||
/**
|
||||
* Returns all defined routes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function routes(): array
|
||||
{
|
||||
|
@ -475,11 +405,9 @@ class Api
|
|||
|
||||
/**
|
||||
* Setter for the authentication callback
|
||||
*
|
||||
* @param \Closure|null $authentication
|
||||
* @return $this
|
||||
*/
|
||||
protected function setAuthentication(Closure $authentication = null)
|
||||
protected function setAuthentication(Closure|null $authentication = null): static
|
||||
{
|
||||
$this->authentication = $authentication;
|
||||
return $this;
|
||||
|
@ -487,11 +415,9 @@ class Api
|
|||
|
||||
/**
|
||||
* Setter for the collections definition
|
||||
*
|
||||
* @param array|null $collections
|
||||
* @return $this
|
||||
*/
|
||||
protected function setCollections(array $collections = null)
|
||||
protected function setCollections(array|null $collections = null): static
|
||||
{
|
||||
if ($collections !== null) {
|
||||
$this->collections = array_change_key_case($collections);
|
||||
|
@ -501,11 +427,9 @@ class Api
|
|||
|
||||
/**
|
||||
* Setter for the injected data
|
||||
*
|
||||
* @param array|null $data
|
||||
* @return $this
|
||||
*/
|
||||
protected function setData(array $data = null)
|
||||
protected function setData(array|null $data = null): static
|
||||
{
|
||||
$this->data = $data ?? [];
|
||||
return $this;
|
||||
|
@ -513,11 +437,9 @@ class Api
|
|||
|
||||
/**
|
||||
* Setter for the debug flag
|
||||
*
|
||||
* @param bool $debug
|
||||
* @return $this
|
||||
*/
|
||||
protected function setDebug(bool $debug = false)
|
||||
protected function setDebug(bool $debug = false): static
|
||||
{
|
||||
$this->debug = $debug;
|
||||
return $this;
|
||||
|
@ -525,11 +447,9 @@ class Api
|
|||
|
||||
/**
|
||||
* Setter for the model definitions
|
||||
*
|
||||
* @param array|null $models
|
||||
* @return $this
|
||||
*/
|
||||
protected function setModels(array $models = null)
|
||||
protected function setModels(array|null $models = null): static
|
||||
{
|
||||
if ($models !== null) {
|
||||
$this->models = array_change_key_case($models);
|
||||
|
@ -540,11 +460,9 @@ class Api
|
|||
|
||||
/**
|
||||
* Setter for the request data
|
||||
*
|
||||
* @param array|null $requestData
|
||||
* @return $this
|
||||
*/
|
||||
protected function setRequestData(array $requestData = null)
|
||||
protected function setRequestData(array|null $requestData = null): static
|
||||
{
|
||||
$defaults = [
|
||||
'query' => [],
|
||||
|
@ -558,11 +476,9 @@ class Api
|
|||
|
||||
/**
|
||||
* Setter for the request method
|
||||
*
|
||||
* @param string|null $requestMethod
|
||||
* @return $this
|
||||
*/
|
||||
protected function setRequestMethod(string $requestMethod = null)
|
||||
protected function setRequestMethod(string|null $requestMethod = null): static
|
||||
{
|
||||
$this->requestMethod = $requestMethod ?? 'GET';
|
||||
return $this;
|
||||
|
@ -570,11 +486,9 @@ class Api
|
|||
|
||||
/**
|
||||
* Setter for the route definitions
|
||||
*
|
||||
* @param array|null $routes
|
||||
* @return $this
|
||||
*/
|
||||
protected function setRoutes(array $routes = null)
|
||||
protected function setRoutes(array|null $routes = null): static
|
||||
{
|
||||
$this->routes = $routes ?? [];
|
||||
return $this;
|
||||
|
@ -582,13 +496,8 @@ class Api
|
|||
|
||||
/**
|
||||
* Renders the API call
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $method
|
||||
* @param array $requestData
|
||||
* @return mixed
|
||||
*/
|
||||
public function render(string $path, $method = 'GET', array $requestData = [])
|
||||
public function render(string $path, string $method = 'GET', array $requestData = [])
|
||||
{
|
||||
try {
|
||||
$result = $this->call($path, $method, $requestData);
|
||||
|
@ -596,13 +505,12 @@ class Api
|
|||
$result = $this->responseForException($e);
|
||||
}
|
||||
|
||||
if ($result === null) {
|
||||
$result = $this->responseFor404();
|
||||
} elseif ($result === false) {
|
||||
$result = $this->responseFor400();
|
||||
} elseif ($result === true) {
|
||||
$result = $this->responseFor200();
|
||||
}
|
||||
$result = match ($result) {
|
||||
null => $this->responseFor404(),
|
||||
false => $this->responseFor400(),
|
||||
true => $this->responseFor200(),
|
||||
default => $result
|
||||
};
|
||||
|
||||
if (is_array($result) === false) {
|
||||
return $result;
|
||||
|
@ -628,8 +536,6 @@ class Api
|
|||
/**
|
||||
* Returns a 200 - ok
|
||||
* response array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function responseFor200(): array
|
||||
{
|
||||
|
@ -643,8 +549,6 @@ class Api
|
|||
/**
|
||||
* Returns a 400 - bad request
|
||||
* response array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function responseFor400(): array
|
||||
{
|
||||
|
@ -658,8 +562,6 @@ class Api
|
|||
/**
|
||||
* Returns a 404 - not found
|
||||
* response array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function responseFor404(): array
|
||||
{
|
||||
|
@ -674,9 +576,6 @@ class Api
|
|||
* Creates the response array for
|
||||
* an exception. Kirby exceptions will
|
||||
* have more information
|
||||
*
|
||||
* @param \Throwable $e
|
||||
* @return array
|
||||
*/
|
||||
public function responseForException(Throwable $e): array
|
||||
{
|
||||
|
@ -696,11 +595,11 @@ class Api
|
|||
'file' => F::relativepath($e->getFile(), $docRoot),
|
||||
'line' => $e->getLine(),
|
||||
'details' => [],
|
||||
'route' => $this->route ? $this->route->pattern() : null
|
||||
'route' => $this->route?->pattern()
|
||||
];
|
||||
|
||||
// extend the information for Kirby Exceptions
|
||||
if (is_a($e, 'Kirby\Exception\Exception') === true) {
|
||||
if ($e instanceof ExceptionException) {
|
||||
$result['key'] = $e->getKey();
|
||||
$result['details'] = $e->getDetails();
|
||||
$result['code'] = $e->getHttpCode();
|
||||
|
@ -726,14 +625,9 @@ class Api
|
|||
* move_uploaded_file() not working with unit test
|
||||
* Added debug parameter for testing purposes as we did in the Email class
|
||||
*
|
||||
* @param \Closure $callback
|
||||
* @param bool $single
|
||||
* @param bool $debug
|
||||
* @return array
|
||||
*
|
||||
* @throws \Exception If request has no files or there was an error with the upload
|
||||
*/
|
||||
public function upload(Closure $callback, $single = false, $debug = false): array
|
||||
public function upload(Closure $callback, bool $single = false, bool $debug = false): array
|
||||
{
|
||||
$trials = 0;
|
||||
$uploads = [];
|
||||
|
@ -757,13 +651,16 @@ class Api
|
|||
|
||||
if ($postMaxSize < $uploadMaxFileSize) {
|
||||
throw new Exception(I18n::translate('upload.error.iniPostSize'));
|
||||
} else {
|
||||
throw new Exception(I18n::translate('upload.error.noFiles'));
|
||||
}
|
||||
|
||||
throw new Exception(I18n::translate('upload.error.noFiles'));
|
||||
}
|
||||
|
||||
foreach ($files as $upload) {
|
||||
if (isset($upload['tmp_name']) === false && is_array($upload)) {
|
||||
if (
|
||||
isset($upload['tmp_name']) === false &&
|
||||
is_array($upload) === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -780,7 +677,10 @@ class Api
|
|||
|
||||
// try to detect the correct mime and add the extension
|
||||
// accordingly. This will avoid .tmp filenames
|
||||
if (empty($extension) === true || in_array($extension, ['tmp', 'temp'])) {
|
||||
if (
|
||||
empty($extension) === true ||
|
||||
in_array($extension, ['tmp', 'temp']) === true
|
||||
) {
|
||||
$mime = F::mime($upload['tmp_name']);
|
||||
$extension = F::mimeToExtension($mime);
|
||||
$filename = F::name($upload['name']) . '.' . $extension;
|
||||
|
@ -792,7 +692,10 @@ class Api
|
|||
|
||||
// move the file to a location including the extension,
|
||||
// for better mime detection
|
||||
if ($debug === false && move_uploaded_file($upload['tmp_name'], $source) === false) {
|
||||
if (
|
||||
$debug === false &&
|
||||
move_uploaded_file($upload['tmp_name'], $source) === false
|
||||
) {
|
||||
throw new Exception(I18n::translate('upload.error.cantMove'));
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Kirby\Api;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
|
@ -19,48 +20,27 @@ use Kirby\Toolkit\Str;
|
|||
*/
|
||||
class Collection
|
||||
{
|
||||
/**
|
||||
* @var \Kirby\Api\Api
|
||||
*/
|
||||
protected $api;
|
||||
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
protected Api $api;
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
protected $select;
|
||||
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* Collection constructor
|
||||
*
|
||||
* @param \Kirby\Api\Api $api
|
||||
* @param mixed|null $data
|
||||
* @param array $schema
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(Api $api, $data, array $schema)
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->data = $data;
|
||||
$this->model = $schema['model'] ?? null;
|
||||
$this->view = $schema['view'] ?? null;
|
||||
$this->api = $api;
|
||||
$this->data = $data;
|
||||
$this->model = $schema['model'] ?? null;
|
||||
$this->select = null;
|
||||
$this->view = $schema['view'] ?? null;
|
||||
|
||||
if ($data === null) {
|
||||
if (is_a($schema['default'] ?? null, 'Closure') === false) {
|
||||
if (($schema['default'] ?? null) instanceof Closure === false) {
|
||||
throw new Exception('Missing collection data');
|
||||
}
|
||||
|
||||
|
@ -69,18 +49,17 @@ class Collection
|
|||
|
||||
if (
|
||||
isset($schema['type']) === true &&
|
||||
is_a($this->data, $schema['type']) === false
|
||||
$this->data instanceof $schema['type'] === false
|
||||
) {
|
||||
throw new Exception('Invalid collection type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array|null $keys
|
||||
* @return $this
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function select($keys = null)
|
||||
public function select($keys = null): static
|
||||
{
|
||||
if ($keys === false) {
|
||||
return $this;
|
||||
|
@ -99,7 +78,6 @@ class Collection
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \Kirby\Exception\NotFoundException
|
||||
* @throws \Exception
|
||||
*/
|
||||
|
@ -125,7 +103,6 @@ class Collection
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \Kirby\Exception\NotFoundException
|
||||
* @throws \Exception
|
||||
*/
|
||||
|
@ -167,10 +144,9 @@ class Collection
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $view
|
||||
* @return $this
|
||||
*/
|
||||
public function view(string $view)
|
||||
public function view(string $view): static
|
||||
{
|
||||
$this->view = $view;
|
||||
return $this;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Kirby\Api;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
|
@ -21,37 +22,15 @@ use Kirby\Toolkit\Str;
|
|||
*/
|
||||
class Model
|
||||
{
|
||||
/**
|
||||
* @var \Kirby\Api\Api
|
||||
*/
|
||||
protected $api;
|
||||
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
protected Api $api;
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* @var array|mixed
|
||||
*/
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
protected $select;
|
||||
|
||||
/**
|
||||
* @var array|mixed
|
||||
*/
|
||||
protected $views;
|
||||
|
||||
/**
|
||||
* Model constructor
|
||||
*
|
||||
* @param \Kirby\Api\Api $api
|
||||
* @param mixed $data
|
||||
* @param array $schema
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(Api $api, $data, array $schema)
|
||||
|
@ -62,12 +41,15 @@ class Model
|
|||
$this->select = $schema['select'] ?? null;
|
||||
$this->views = $schema['views'] ?? [];
|
||||
|
||||
if ($this->select === null && array_key_exists('default', $this->views)) {
|
||||
if (
|
||||
$this->select === null &&
|
||||
array_key_exists('default', $this->views)
|
||||
) {
|
||||
$this->view('default');
|
||||
}
|
||||
|
||||
if ($data === null) {
|
||||
if (is_a($schema['default'] ?? null, 'Closure') === false) {
|
||||
if (($schema['default'] ?? null) instanceof Closure === false) {
|
||||
throw new Exception('Missing model data');
|
||||
}
|
||||
|
||||
|
@ -76,18 +58,17 @@ class Model
|
|||
|
||||
if (
|
||||
isset($schema['type']) === true &&
|
||||
is_a($this->data, $schema['type']) === false
|
||||
$this->data instanceof $schema['type'] === false
|
||||
) {
|
||||
throw new Exception(sprintf('Invalid model type "%s" expected: "%s"', get_class($this->data), $schema['type']));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $keys
|
||||
* @return $this
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function select($keys = null)
|
||||
public function select($keys = null): static
|
||||
{
|
||||
if ($keys === false) {
|
||||
return $this;
|
||||
|
@ -106,7 +87,6 @@ class Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function selection(): array
|
||||
|
@ -153,7 +133,6 @@ class Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \Kirby\Exception\NotFoundException
|
||||
* @throws \Exception
|
||||
*/
|
||||
|
@ -163,7 +142,10 @@ class Model
|
|||
$result = [];
|
||||
|
||||
foreach ($this->fields as $key => $resolver) {
|
||||
if (array_key_exists($key, $select) === false || is_a($resolver, 'Closure') === false) {
|
||||
if (
|
||||
array_key_exists($key, $select) === false ||
|
||||
$resolver instanceof Closure === false
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -174,8 +156,8 @@ class Model
|
|||
}
|
||||
|
||||
if (
|
||||
is_a($value, 'Kirby\Api\Collection') === true ||
|
||||
is_a($value, 'Kirby\Api\Model') === true
|
||||
$value instanceof Collection ||
|
||||
$value instanceof self
|
||||
) {
|
||||
$selection = $select[$key];
|
||||
|
||||
|
@ -199,7 +181,6 @@ class Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @throws \Kirby\Exception\NotFoundException
|
||||
* @throws \Exception
|
||||
*/
|
||||
|
@ -224,11 +205,10 @@ class Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return $this
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function view(string $name)
|
||||
public function view(string $name): static
|
||||
{
|
||||
if ($name === 'any') {
|
||||
return $this->select(null);
|
||||
|
|
90
kirby/src/Blueprint/Collection.php
Normal file
90
kirby/src/Blueprint/Collection.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
use Kirby\Cms\Collection as BaseCollection;
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
use Kirby\Toolkit\A;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* Typed collection
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Collection extends BaseCollection
|
||||
{
|
||||
/**
|
||||
* The expected object type
|
||||
*/
|
||||
public const TYPE = Node::class;
|
||||
|
||||
public function __construct(array $objects = [])
|
||||
{
|
||||
foreach ($objects as $object) {
|
||||
$this->__set($object->id, $object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Kirby Collection class only shows the key to
|
||||
* avoid huge tress with dump, but for the blueprint
|
||||
* collections this is really not useful
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return A::map($this->data, fn ($item) => (array)$item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the type of every item that is being
|
||||
* added to the collection. They cneed to have
|
||||
* the class defined by static::TYPE.
|
||||
*/
|
||||
public function __set(string $key, $value): void
|
||||
{
|
||||
if (
|
||||
is_a($value, static::TYPE) === false
|
||||
) {
|
||||
throw new TypeError('Each value in the collection must be an instance of ' . static::TYPE);
|
||||
}
|
||||
|
||||
parent::__set($key, $value);
|
||||
}
|
||||
|
||||
public static function factory(array $items)
|
||||
{
|
||||
$collection = new static();
|
||||
$className = static::TYPE;
|
||||
|
||||
foreach ($items as $id => $item) {
|
||||
if (is_array($item) === true) {
|
||||
$item['id'] ??= $id;
|
||||
$item = $className::factory($item);
|
||||
$collection->__set($item->id, $item);
|
||||
} else {
|
||||
$collection->__set($id, $className::factory($item));
|
||||
}
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
public function render(ModelWithContent $model)
|
||||
{
|
||||
$props = [];
|
||||
|
||||
foreach ($this->data as $key => $item) {
|
||||
$props[$key] = $item->render($model);
|
||||
}
|
||||
|
||||
return $props;
|
||||
}
|
||||
}
|
75
kirby/src/Blueprint/Config.php
Normal file
75
kirby/src/Blueprint/Config.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Data\Yaml;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Filesystem\F;
|
||||
|
||||
/**
|
||||
* Config
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Config
|
||||
{
|
||||
public string $file;
|
||||
public string $id;
|
||||
public string|array|Closure|null $plugin;
|
||||
public string $root;
|
||||
|
||||
public function __construct(
|
||||
public string $path
|
||||
) {
|
||||
$kirby = App::instance();
|
||||
|
||||
$this->id = basename($this->path);
|
||||
$this->root = $kirby->root('blueprints');
|
||||
$this->file = $this->root . '/' . $this->path . '.yml';
|
||||
$this->plugin = $kirby->extension('blueprints', $this->path);
|
||||
}
|
||||
|
||||
public function read(): array
|
||||
{
|
||||
if (F::exists($this->file, $this->root) === true) {
|
||||
return $this->unpack($this->file);
|
||||
}
|
||||
|
||||
return $this->unpack($this->plugin);
|
||||
}
|
||||
|
||||
public function write(array $props): bool
|
||||
{
|
||||
return Yaml::write($this->file, $props);
|
||||
}
|
||||
|
||||
public function unpack(string|array|Closure|null $extension): array
|
||||
{
|
||||
return match (true) {
|
||||
// extension does not exist
|
||||
is_null($extension)
|
||||
=> throw new NotFoundException('"' . $this->path . '" could not be found'),
|
||||
|
||||
// extension is stored as a file path
|
||||
is_string($extension)
|
||||
=> Yaml::read($extension),
|
||||
|
||||
// extension is a callback to be resolved
|
||||
is_callable($extension)
|
||||
=> $extension(App::instance()),
|
||||
|
||||
// extension is already defined as array
|
||||
default
|
||||
=> $extension
|
||||
};
|
||||
}
|
||||
}
|
65
kirby/src/Blueprint/Extension.php
Normal file
65
kirby/src/Blueprint/Extension.php
Normal file
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
/**
|
||||
* Extension
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Extension
|
||||
{
|
||||
public function __construct(
|
||||
public string $path
|
||||
) {
|
||||
}
|
||||
|
||||
public static function apply(array $props): array
|
||||
{
|
||||
if (isset($props['extends']) === false) {
|
||||
return $props;
|
||||
}
|
||||
|
||||
// already extended
|
||||
if (is_a($props['extends'], Extension::class) === true) {
|
||||
return $props;
|
||||
}
|
||||
|
||||
$extension = new static($props['extends']);
|
||||
return $extension->extend($props);
|
||||
}
|
||||
|
||||
public function extend(array $props): array
|
||||
{
|
||||
$props = array_replace_recursive(
|
||||
$this->read(),
|
||||
$props
|
||||
);
|
||||
|
||||
$props['extends'] = $this;
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
public static function factory(string|array $path): static
|
||||
{
|
||||
if (is_string($path) === true) {
|
||||
return new static(path: $path);
|
||||
}
|
||||
|
||||
return new static(...$path);
|
||||
}
|
||||
|
||||
public function read(): array
|
||||
{
|
||||
$config = new Config($this->path);
|
||||
return $config->read();
|
||||
}
|
||||
}
|
119
kirby/src/Blueprint/Factory.php
Normal file
119
kirby/src/Blueprint/Factory.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
use ReflectionException;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionProperty;
|
||||
use ReflectionUnionType;
|
||||
|
||||
/**
|
||||
* Factory
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Factory
|
||||
{
|
||||
/**
|
||||
* Resolves the properties by
|
||||
* applying a map of factories (propName => class)
|
||||
*/
|
||||
public static function apply(array $properties, array $factories): array
|
||||
{
|
||||
foreach ($factories as $property => $class) {
|
||||
// skip non-existing properties, empty properties
|
||||
// or properties that are matching objects
|
||||
if (
|
||||
isset($properties[$property]) === false ||
|
||||
$properties[$property] === null ||
|
||||
is_a($properties[$property], $class) === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$properties[$property] = $class::factory($properties[$property]);
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
public static function forNamedType(ReflectionNamedType|null $type, $value)
|
||||
{
|
||||
// get the class name for the single type
|
||||
$className = $type->getName();
|
||||
|
||||
// check if there's a factory for the value
|
||||
if (method_exists($className, 'factory') === true) {
|
||||
return $className::factory($value);
|
||||
}
|
||||
|
||||
// try to assign the value directly and trust
|
||||
// in PHP's type system.
|
||||
return $value;
|
||||
}
|
||||
|
||||
public static function forProperties(string $class, array $properties): array
|
||||
{
|
||||
foreach ($properties as $property => $value) {
|
||||
try {
|
||||
$properties[$property] = static::forProperty($class, $property, $value);
|
||||
} catch (ReflectionException $e) {
|
||||
// the property does not exist
|
||||
unset($properties[$property]);
|
||||
}
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
public static function forProperty(string $class, string $property, $value)
|
||||
{
|
||||
if (is_null($value) === true) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// instantly assign objects
|
||||
// PHP's type system will find issues automatically
|
||||
if (is_object($value) === true) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// get the type for the property
|
||||
$reflection = new ReflectionProperty($class, $property);
|
||||
$propType = $reflection->getType();
|
||||
|
||||
// no type given
|
||||
if ($propType === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// union types
|
||||
if (is_a($propType, ReflectionUnionType::class) === true) {
|
||||
return static::forUnionType($propType, $value);
|
||||
}
|
||||
|
||||
return static::forNamedType($propType, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* For properties with union types,
|
||||
* the first named type is used to create
|
||||
* the factory or pass a built-in value
|
||||
*/
|
||||
public static function forUnionType(ReflectionUnionType $type, $value)
|
||||
{
|
||||
return static::forNamedType($type->getTypes()[0], $value);
|
||||
}
|
||||
|
||||
public static function make(string $class, array $properties): object
|
||||
{
|
||||
return new $class(...static::forProperties($class, $properties));
|
||||
}
|
||||
}
|
118
kirby/src/Blueprint/Node.php
Normal file
118
kirby/src/Blueprint/Node.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
|
||||
/**
|
||||
* A node of the blueprint
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Node
|
||||
{
|
||||
public const TYPE = 'node';
|
||||
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public Extension|null $extends = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamic getter for properties
|
||||
*/
|
||||
public function __call(string $name, array $args)
|
||||
{
|
||||
$this->defaults();
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply default values
|
||||
*/
|
||||
public function defaults(): static
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance by a set of array properties.
|
||||
*/
|
||||
public static function factory(array $props): static
|
||||
{
|
||||
$props = Extension::apply($props);
|
||||
$props = static::polyfill($props);
|
||||
return Factory::make(static::class, $props);
|
||||
}
|
||||
|
||||
|
||||
public static function load(string|array $props): static
|
||||
{
|
||||
// load by path
|
||||
if (is_string($props) === true) {
|
||||
$props = static::loadProps($props);
|
||||
}
|
||||
|
||||
return static::factory($props);
|
||||
}
|
||||
|
||||
public static function loadProps(string $path): array
|
||||
{
|
||||
$config = new Config($path);
|
||||
$props = $config->read();
|
||||
|
||||
// add the id if it's not set yet
|
||||
$props['id'] ??= basename($path);
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional method that runs before static::factory sends
|
||||
* its properties to the instance. This is perfect to clean
|
||||
* up props or keep deprecated props compatible.
|
||||
*/
|
||||
public static function polyfill(array $props): array
|
||||
{
|
||||
return $props;
|
||||
}
|
||||
|
||||
public function render(ModelWithContent $model)
|
||||
{
|
||||
// apply default values
|
||||
$this->defaults();
|
||||
|
||||
$array = [];
|
||||
|
||||
// go through all public properties
|
||||
foreach (get_object_vars($this) as $key => $value) {
|
||||
if (is_object($value) === false && is_resource($value) === false) {
|
||||
$array[$key] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method_exists($value, 'render') === true) {
|
||||
$array[$key] = $value->render($model);
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal setter for properties
|
||||
*/
|
||||
public function set(string $property, $value): static
|
||||
{
|
||||
$this->$property = Factory::forProperty(static::class, $property, $value);
|
||||
return $this;
|
||||
}
|
||||
}
|
44
kirby/src/Blueprint/NodeI18n.php
Normal file
44
kirby/src/Blueprint/NodeI18n.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
/**
|
||||
* Translatable node property
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class NodeI18n extends NodeProperty
|
||||
{
|
||||
public function __construct(
|
||||
public array $translations,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function factory($value = null): static|null
|
||||
{
|
||||
if ($value === false || $value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value) === false) {
|
||||
$value = ['en' => $value];
|
||||
}
|
||||
|
||||
return new static($value);
|
||||
}
|
||||
|
||||
public function render(ModelWithContent $model): string|null
|
||||
{
|
||||
return I18n::translate($this->translations, $this->translations);
|
||||
}
|
||||
}
|
27
kirby/src/Blueprint/NodeIcon.php
Normal file
27
kirby/src/Blueprint/NodeIcon.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
/**
|
||||
* Custom emoji or icon from the Kirby iconset
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class NodeIcon extends NodeString
|
||||
{
|
||||
public static function field()
|
||||
{
|
||||
$field = parent::field();
|
||||
$field->id = 'icon';
|
||||
$field->label->translations = ['en' => 'Icon'];
|
||||
|
||||
return $field;
|
||||
}
|
||||
}
|
27
kirby/src/Blueprint/NodeProperty.php
Normal file
27
kirby/src/Blueprint/NodeProperty.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
|
||||
/**
|
||||
* Represents a property for a node
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
abstract class NodeProperty
|
||||
{
|
||||
abstract public static function factory($value = null): static|null;
|
||||
|
||||
public function render(ModelWithContent $model)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
39
kirby/src/Blueprint/NodeString.php
Normal file
39
kirby/src/Blueprint/NodeString.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
|
||||
/**
|
||||
* Simple string blueprint node
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class NodeString extends NodeProperty
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function factory($value = null): static|null
|
||||
{
|
||||
if ($value === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return new static($value);
|
||||
}
|
||||
|
||||
public function render(ModelWithContent $model): string|null
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
30
kirby/src/Blueprint/NodeText.php
Normal file
30
kirby/src/Blueprint/NodeText.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Blueprint;
|
||||
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
|
||||
/**
|
||||
* The text node is translatable
|
||||
* and will parse query template strings
|
||||
*
|
||||
* @package Kirby Blueprint
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* // TODO: include in test coverage in 3.9
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class NodeText extends NodeI18n
|
||||
{
|
||||
public function render(ModelWithContent $model): ?string
|
||||
{
|
||||
if ($text = parent::render($model)) {
|
||||
return $model->toSafeString($text);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
|
@ -15,11 +15,17 @@ use APCUIterator;
|
|||
*/
|
||||
class ApcuCache extends Cache
|
||||
{
|
||||
/**
|
||||
* Returns whether the cache is ready to
|
||||
* store values
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return apcu_enabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an item exists in the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(string $key): bool
|
||||
{
|
||||
|
@ -29,24 +35,19 @@ class ApcuCache extends Cache
|
|||
/**
|
||||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
if (empty($this->options['prefix']) === false) {
|
||||
return apcu_delete(new APCUIterator('!^' . preg_quote($this->options['prefix']) . '!'));
|
||||
} else {
|
||||
return apcu_clear_cache();
|
||||
}
|
||||
|
||||
return apcu_clear_cache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
|
@ -56,13 +57,11 @@ class ApcuCache extends Cache
|
|||
/**
|
||||
* Internal method to retrieve the raw cache value;
|
||||
* needs to return a Value object or null if not found
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cache\Value|null
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
public function retrieve(string $key): Value|null
|
||||
{
|
||||
return Value::fromJson(apcu_fetch($this->key($key)));
|
||||
$value = apcu_fetch($this->key($key));
|
||||
return Value::fromJson($value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,14 +72,12 @@ class ApcuCache extends Cache
|
|||
* // put an item in the cache for 15 minutes
|
||||
* $cache->set('value', 'my value', 15);
|
||||
* </code>
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $minutes
|
||||
* @return bool
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
return apcu_store($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes));
|
||||
$key = $this->key($key);
|
||||
$value = (new Value($value, $minutes))->toJson();
|
||||
$expires = $this->expiration($minutes);
|
||||
return apcu_store($key, $value, $expires);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Cache foundation
|
||||
* This abstract class is used as
|
||||
|
@ -18,14 +20,11 @@ abstract class Cache
|
|||
{
|
||||
/**
|
||||
* Stores all options for the driver
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* Sets all parameters which are needed to connect to the cache storage
|
||||
*
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
|
@ -33,87 +32,47 @@ abstract class Cache
|
|||
}
|
||||
|
||||
/**
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
* returns whether the operation was successful;
|
||||
* this needs to be defined by the driver
|
||||
*
|
||||
* <code>
|
||||
* // put an item in the cache for 15 minutes
|
||||
* $cache->set('value', 'my value', 15);
|
||||
* </code>
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $minutes
|
||||
* @return bool
|
||||
* Checks when the cache has been created;
|
||||
* returns the creation timestamp on success
|
||||
* and false if the item does not exist
|
||||
*/
|
||||
abstract public function set(string $key, $value, int $minutes = 0): bool;
|
||||
|
||||
/**
|
||||
* Adds the prefix to the key if given
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function key(string $key): string
|
||||
public function created(string $key): int|false
|
||||
{
|
||||
if (empty($this->options['prefix']) === false) {
|
||||
$key = $this->options['prefix'] . '/' . $key;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to retrieve the raw cache value;
|
||||
* needs to return a Value object or null if not found;
|
||||
* this needs to be defined by the driver
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cache\Value|null
|
||||
*/
|
||||
abstract public function retrieve(string $key);
|
||||
|
||||
/**
|
||||
* Gets an item from the cache
|
||||
*
|
||||
* <code>
|
||||
* // get an item from the cache driver
|
||||
* $value = $cache->get('value');
|
||||
*
|
||||
* // return a default value if the requested item isn't cached
|
||||
* $value = $cache->get('value', 'default value');
|
||||
* </code>
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key, $default = null)
|
||||
{
|
||||
// get the Value
|
||||
// get the Value object
|
||||
$value = $this->retrieve($key);
|
||||
|
||||
// check for a valid cache value
|
||||
if (!is_a($value, 'Kirby\Cache\Value')) {
|
||||
return $default;
|
||||
// check for a valid Value object
|
||||
if ($value instanceof Value === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove the item if it is expired
|
||||
if ($value->expires() > 0 && time() >= $value->expires()) {
|
||||
$this->remove($key);
|
||||
return $default;
|
||||
}
|
||||
|
||||
// return the pure value
|
||||
return $value->value();
|
||||
// return the expires timestamp
|
||||
return $value->created();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the cache is ready to
|
||||
* store values
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
// TODO: Make this method abstract in a future
|
||||
// release to ensure that cache drivers override it;
|
||||
// until then, we assume that the cache is enabled
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an item exists in the cache
|
||||
*/
|
||||
public function exists(string $key): bool
|
||||
{
|
||||
return $this->expired($key) === false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the expiration timestamp
|
||||
*
|
||||
* @param int $minutes
|
||||
* @return int
|
||||
*/
|
||||
protected function expiration(int $minutes = 0): int
|
||||
{
|
||||
|
@ -130,17 +89,14 @@ abstract class Cache
|
|||
* Checks when an item in the cache expires;
|
||||
* returns the expiry timestamp on success, null if the
|
||||
* item never expires and false if the item does not exist
|
||||
*
|
||||
* @param string $key
|
||||
* @return int|null|false
|
||||
*/
|
||||
public function expires(string $key)
|
||||
public function expires(string $key): int|false|null
|
||||
{
|
||||
// get the Value object
|
||||
$value = $this->retrieve($key);
|
||||
|
||||
// check for a valid Value object
|
||||
if (!is_a($value, 'Kirby\Cache\Value')) {
|
||||
if ($value instanceof Value === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -150,9 +106,6 @@ abstract class Cache
|
|||
|
||||
/**
|
||||
* Checks if an item in the cache is expired
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function expired(string $key): bool
|
||||
{
|
||||
|
@ -160,83 +113,126 @@ abstract class Cache
|
|||
|
||||
if ($expires === null) {
|
||||
return false;
|
||||
} elseif (!is_int($expires)) {
|
||||
return true;
|
||||
} else {
|
||||
return time() >= $expires;
|
||||
}
|
||||
|
||||
if (is_int($expires) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return time() >= $expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks when the cache has been created;
|
||||
* returns the creation timestamp on success
|
||||
* and false if the item does not exist
|
||||
*
|
||||
* @param string $key
|
||||
* @return int|false
|
||||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful;
|
||||
* this needs to be defined by the driver
|
||||
*/
|
||||
public function created(string $key)
|
||||
abstract public function flush(): bool;
|
||||
|
||||
/**
|
||||
* Gets an item from the cache
|
||||
*
|
||||
* <code>
|
||||
* // get an item from the cache driver
|
||||
* $value = $cache->get('value');
|
||||
*
|
||||
* // return a default value if the requested item isn't cached
|
||||
* $value = $cache->get('value', 'default value');
|
||||
* </code>
|
||||
*/
|
||||
public function get(string $key, $default = null)
|
||||
{
|
||||
// get the Value object
|
||||
// get the Value
|
||||
$value = $this->retrieve($key);
|
||||
|
||||
// check for a valid Value object
|
||||
if (!is_a($value, 'Kirby\Cache\Value')) {
|
||||
return false;
|
||||
// check for a valid cache value
|
||||
if ($value instanceof Value === false) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// return the expires timestamp
|
||||
return $value->created();
|
||||
// remove the item if it is expired
|
||||
if ($value->expires() > 0 && time() >= $value->expires()) {
|
||||
$this->remove($key);
|
||||
return $default;
|
||||
}
|
||||
|
||||
// return the pure value
|
||||
return $value->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a value by either getting it from the cache
|
||||
* or via the callback function which then is stored in
|
||||
* the cache for future retrieval. This method cannot be
|
||||
* used for `null` as value to be cached.
|
||||
* @since 3.8.0
|
||||
*/
|
||||
public function getOrSet(
|
||||
string $key,
|
||||
Closure $result,
|
||||
int $minutes = 0
|
||||
) {
|
||||
$value = $this->get($key);
|
||||
$result = $value ?? $result();
|
||||
|
||||
if ($value === null) {
|
||||
$this->set($key, $result, $minutes);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the prefix to the key if given
|
||||
*/
|
||||
protected function key(string $key): string
|
||||
{
|
||||
if (empty($this->options['prefix']) === false) {
|
||||
$key = $this->options['prefix'] . '/' . $key;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternate version for Cache::created($key)
|
||||
*
|
||||
* @param string $key
|
||||
* @return int|false
|
||||
*/
|
||||
public function modified(string $key)
|
||||
public function modified(string $key): int|false
|
||||
{
|
||||
return static::created($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an item exists in the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
* Returns all passed cache options
|
||||
*/
|
||||
public function exists(string $key): bool
|
||||
public function options(): array
|
||||
{
|
||||
return $this->expired($key) === false;
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful;
|
||||
* this needs to be defined by the driver
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function remove(string $key): bool;
|
||||
|
||||
/**
|
||||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful;
|
||||
* Internal method to retrieve the raw cache value;
|
||||
* needs to return a Value object or null if not found;
|
||||
* this needs to be defined by the driver
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function flush(): bool;
|
||||
abstract public function retrieve(string $key): Value|null;
|
||||
|
||||
/**
|
||||
* Returns all passed cache options
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
* returns whether the operation was successful;
|
||||
* this needs to be defined by the driver
|
||||
*
|
||||
* @return array
|
||||
* <code>
|
||||
* // put an item in the cache for 15 minutes
|
||||
* $cache->set('value', 'my value', 15);
|
||||
* </code>
|
||||
*/
|
||||
public function options(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
abstract public function set(string $key, $value, int $minutes = 0): bool;
|
||||
}
|
||||
|
|
|
@ -20,10 +20,8 @@ class FileCache extends Cache
|
|||
{
|
||||
/**
|
||||
* Full root including prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $root;
|
||||
protected string $root;
|
||||
|
||||
/**
|
||||
* Sets all parameters which are needed for the file cache
|
||||
|
@ -44,6 +42,7 @@ class FileCache extends Cache
|
|||
|
||||
// build the full root including prefix
|
||||
$this->root = $this->options['root'];
|
||||
|
||||
if (empty($this->options['prefix']) === false) {
|
||||
$this->root .= '/' . $this->options['prefix'];
|
||||
}
|
||||
|
@ -52,10 +51,17 @@ class FileCache extends Cache
|
|||
Dir::make($this->root, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the cache is ready to
|
||||
* store values
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return is_writable($this->root) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full root including prefix
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
|
@ -64,9 +70,6 @@ class FileCache extends Cache
|
|||
|
||||
/**
|
||||
* Returns the full path to a file for a given key
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function file(string $key): string
|
||||
{
|
||||
|
@ -108,9 +111,9 @@ class FileCache extends Cache
|
|||
|
||||
if (isset($this->options['extension'])) {
|
||||
return $file . '.' . $this->options['extension'];
|
||||
} else {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -121,11 +124,6 @@ class FileCache extends Cache
|
|||
* // put an item in the cache for 15 minutes
|
||||
* $cache->set('value', 'my value', 15);
|
||||
* </code>
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $minutes
|
||||
* @return bool
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
|
@ -137,11 +135,8 @@ class FileCache extends Cache
|
|||
/**
|
||||
* Internal method to retrieve the raw cache value;
|
||||
* needs to return a Value object or null if not found
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cache\Value|null
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
public function retrieve(string $key): Value|null
|
||||
{
|
||||
$file = $this->file($key);
|
||||
$value = F::read($file);
|
||||
|
@ -153,11 +148,8 @@ class FileCache extends Cache
|
|||
* Checks when the cache has been created;
|
||||
* returns the creation timestamp on success
|
||||
* and false if the item does not exist
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function created(string $key)
|
||||
public function created(string $key): int|false
|
||||
{
|
||||
// use the modification timestamp
|
||||
// as indicator when the cache has been created/overwritten
|
||||
|
@ -165,15 +157,12 @@ class FileCache extends Cache
|
|||
|
||||
// get the file for this cache key
|
||||
$file = $this->file($key);
|
||||
return file_exists($file) ? filemtime($this->file($key)) : false;
|
||||
return file_exists($file) ? filemtime($file) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
|
@ -190,9 +179,6 @@ class FileCache extends Cache
|
|||
/**
|
||||
* Removes empty directories safely by checking each directory
|
||||
* up to the root directory
|
||||
*
|
||||
* @param string $dir
|
||||
* @return void
|
||||
*/
|
||||
protected function removeEmptyDirectories(string $dir): void
|
||||
{
|
||||
|
@ -202,7 +188,13 @@ class FileCache extends Cache
|
|||
|
||||
// checks all directory segments until reaching the root directory
|
||||
while (Str::startsWith($dir, $this->root()) === true && $dir !== $this->root()) {
|
||||
$files = array_diff(scandir($dir) ?? [], ['.', '..']);
|
||||
$files = scandir($dir);
|
||||
|
||||
if ($files === false) {
|
||||
$files = []; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$files = array_diff($files, ['.', '..']);
|
||||
|
||||
if (empty($files) === true && Dir::remove($dir) === true) {
|
||||
// continue with the next level up
|
||||
|
@ -212,7 +204,7 @@ class FileCache extends Cache
|
|||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) { // @codeCoverageIgnore
|
||||
} catch (Exception) { // @codeCoverageIgnore
|
||||
// silently stops the process
|
||||
}
|
||||
}
|
||||
|
@ -220,12 +212,13 @@ class FileCache extends Cache
|
|||
/**
|
||||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
if (Dir::remove($this->root) === true && Dir::make($this->root) === true) {
|
||||
if (
|
||||
Dir::remove($this->root) === true &&
|
||||
Dir::make($this->root) === true
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,10 +16,14 @@ use Memcached as MemcachedExt;
|
|||
class MemCached extends Cache
|
||||
{
|
||||
/**
|
||||
* store for the memcache connection
|
||||
* @var \Memcached
|
||||
* Store for the memcache connection
|
||||
*/
|
||||
protected $connection;
|
||||
protected MemcachedExt $connection;
|
||||
|
||||
/**
|
||||
* Stores whether the connection was successful
|
||||
*/
|
||||
protected bool $enabled;
|
||||
|
||||
/**
|
||||
* Sets all parameters which are needed to connect to Memcached
|
||||
|
@ -39,7 +43,19 @@ class MemCached extends Cache
|
|||
parent::__construct(array_merge($defaults, $options));
|
||||
|
||||
$this->connection = new MemcachedExt();
|
||||
$this->connection->addServer($this->options['host'], $this->options['port']);
|
||||
$this->enabled = $this->connection->addServer(
|
||||
$this->options['host'],
|
||||
$this->options['port']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the cache is ready to
|
||||
* store values
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,35 +66,28 @@ class MemCached extends Cache
|
|||
* // put an item in the cache for 15 minutes
|
||||
* $cache->set('value', 'my value', 15);
|
||||
* </code>
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $minutes
|
||||
* @return bool
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
return $this->connection->set($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes));
|
||||
$key = $this->key($key);
|
||||
$value = (new Value($value, $minutes))->toJson();
|
||||
$expires = $this->expiration($minutes);
|
||||
return $this->connection->set($key, $value, $expires);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to retrieve the raw cache value;
|
||||
* needs to return a Value object or null if not found
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cache\Value|null
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
public function retrieve(string $key): Value|null
|
||||
{
|
||||
return Value::fromJson($this->connection->get($this->key($key)));
|
||||
$value = $this->connection->get($this->key($key));
|
||||
return Value::fromJson($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
|
@ -89,8 +98,6 @@ class MemCached extends Cache
|
|||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful;
|
||||
* WARNING: Memcached only supports flushing the whole cache at once!
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
|
|
|
@ -15,9 +15,17 @@ class MemoryCache extends Cache
|
|||
{
|
||||
/**
|
||||
* Cache data
|
||||
* @var array
|
||||
*/
|
||||
protected $store = [];
|
||||
protected array $store = [];
|
||||
|
||||
/**
|
||||
* Returns whether the cache is ready to
|
||||
* store values
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
|
@ -27,11 +35,6 @@ class MemoryCache extends Cache
|
|||
* // put an item in the cache for 15 minutes
|
||||
* $cache->set('value', 'my value', 15);
|
||||
* </code>
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $minutes
|
||||
* @return bool
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
|
@ -42,11 +45,8 @@ class MemoryCache extends Cache
|
|||
/**
|
||||
* Internal method to retrieve the raw cache value;
|
||||
* needs to return a Value object or null if not found
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cache\Value|null
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
public function retrieve(string $key): Value|null
|
||||
{
|
||||
return $this->store[$key] ?? null;
|
||||
}
|
||||
|
@ -54,25 +54,20 @@ class MemoryCache extends Cache
|
|||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
if (isset($this->store[$key])) {
|
||||
unset($this->store[$key]);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
|
|
|
@ -13,6 +13,15 @@ namespace Kirby\Cache;
|
|||
*/
|
||||
class NullCache extends Cache
|
||||
{
|
||||
/**
|
||||
* Returns whether the cache is ready to
|
||||
* store values
|
||||
*/
|
||||
public function enabled(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
* returns whether the operation was successful
|
||||
|
@ -21,11 +30,6 @@ class NullCache extends Cache
|
|||
* // put an item in the cache for 15 minutes
|
||||
* $cache->set('value', 'my value', 15);
|
||||
* </code>
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @param int $minutes
|
||||
* @return bool
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
|
@ -35,11 +39,8 @@ class NullCache extends Cache
|
|||
/**
|
||||
* Internal method to retrieve the raw cache value;
|
||||
* needs to return a Value object or null if not found
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cache\Value|null
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
public function retrieve(string $key): Value|null
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
@ -47,9 +48,6 @@ class NullCache extends Cache
|
|||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
|
@ -59,8 +57,6 @@ class NullCache extends Cache
|
|||
/**
|
||||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
|
|
|
@ -19,7 +19,6 @@ class Value
|
|||
{
|
||||
/**
|
||||
* Cached value
|
||||
* @var mixed
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
|
@ -27,35 +26,30 @@ class Value
|
|||
* the number of minutes until the value expires
|
||||
* @todo Rename this property to $expiry to reflect
|
||||
* both minutes and absolute timestamps
|
||||
* @var int
|
||||
*/
|
||||
protected $minutes;
|
||||
protected int $minutes;
|
||||
|
||||
/**
|
||||
* Creation timestamp
|
||||
* @var int
|
||||
*/
|
||||
protected $created;
|
||||
protected int $created;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param int $minutes the number of minutes until the value expires
|
||||
* or an absolute UNIX timestamp
|
||||
* @param int $created the UNIX timestamp when the value has been created
|
||||
*/
|
||||
public function __construct($value, int $minutes = 0, int $created = null)
|
||||
public function __construct($value, int $minutes = 0, int|null $created = null)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->minutes = $minutes ?? 0;
|
||||
$this->minutes = $minutes;
|
||||
$this->created = $created ?? time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the creation date as UNIX timestamp
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function created(): int
|
||||
{
|
||||
|
@ -65,10 +59,8 @@ class Value
|
|||
/**
|
||||
* Returns the expiration date as UNIX timestamp or
|
||||
* null if the value never expires
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function expires(): ?int
|
||||
public function expires(): int|null
|
||||
{
|
||||
// 0 = keep forever
|
||||
if ($this->minutes === 0) {
|
||||
|
@ -85,41 +77,37 @@ class Value
|
|||
|
||||
/**
|
||||
* Creates a value object from an array
|
||||
*
|
||||
* @param array $array
|
||||
* @return static
|
||||
*/
|
||||
public static function fromArray(array $array)
|
||||
public static function fromArray(array $array): static
|
||||
{
|
||||
return new static($array['value'] ?? null, $array['minutes'] ?? 0, $array['created'] ?? null);
|
||||
return new static(
|
||||
$array['value'] ?? null,
|
||||
$array['minutes'] ?? 0,
|
||||
$array['created'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a value object from a JSON string;
|
||||
* returns null on error
|
||||
*
|
||||
* @param string $json
|
||||
* @return static|null
|
||||
*/
|
||||
public static function fromJson(string $json)
|
||||
public static function fromJson(string $json): static|null
|
||||
{
|
||||
try {
|
||||
$array = json_decode($json, true);
|
||||
|
||||
if (is_array($array)) {
|
||||
if (is_array($array) === true) {
|
||||
return static::fromArray($array);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
|
||||
return null;
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object to a JSON string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
|
@ -128,8 +116,6 @@ class Value
|
|||
|
||||
/**
|
||||
* Converts the object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
|
@ -142,8 +128,6 @@ class Value
|
|||
|
||||
/**
|
||||
* Returns the pure value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function value()
|
||||
{
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Kirby\Cms;
|
|||
use Kirby\Api\Api as BaseApi;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Form\Form;
|
||||
use Kirby\Session\Session;
|
||||
|
||||
/**
|
||||
* Api
|
||||
|
@ -25,38 +26,30 @@ class Api extends BaseApi
|
|||
/**
|
||||
* Execute an API call for the given path,
|
||||
* request method and optional request data
|
||||
*
|
||||
* @param string|null $path
|
||||
* @param string $method
|
||||
* @param array $requestData
|
||||
* @return mixed
|
||||
*/
|
||||
public function call(string $path = null, string $method = 'GET', array $requestData = [])
|
||||
{
|
||||
public function call(
|
||||
string|null $path = null,
|
||||
string $method = 'GET',
|
||||
array $requestData = []
|
||||
) {
|
||||
$this->setRequestMethod($method);
|
||||
$this->setRequestData($requestData);
|
||||
|
||||
$this->kirby->setCurrentLanguage($this->language());
|
||||
|
||||
$allowImpersonation = $this->kirby()->option('api.allowImpersonation', false);
|
||||
if ($user = $this->kirby->user(null, $allowImpersonation)) {
|
||||
$translation = $user->language();
|
||||
} else {
|
||||
$translation = $this->kirby->panelLanguage();
|
||||
}
|
||||
|
||||
$translation = $this->kirby->user(null, $allowImpersonation)?->language();
|
||||
$translation ??= $this->kirby->panelLanguage();
|
||||
$this->kirby->setCurrentTranslation($translation);
|
||||
|
||||
return parent::call($path, $method, $requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $model
|
||||
* @param string $name
|
||||
* @param string|null $path
|
||||
* @return mixed
|
||||
* @throws \Kirby\Exception\NotFoundException if the field type cannot be found or the field cannot be loaded
|
||||
*/
|
||||
public function fieldApi($model, string $name, string $path = null)
|
||||
public function fieldApi($model, string $name, string|null $path = null)
|
||||
{
|
||||
$field = Form::for($model)->field($name);
|
||||
|
||||
|
@ -75,11 +68,9 @@ class Api extends BaseApi
|
|||
* parent path and filename
|
||||
*
|
||||
* @param string|null $path Path to file's parent model
|
||||
* @param string $filename Filename
|
||||
* @return \Kirby\Cms\File|null
|
||||
* @throws \Kirby\Exception\NotFoundException if the file cannot be found
|
||||
*/
|
||||
public function file(string $path = null, string $filename)
|
||||
public function file(string|null $path = null, string $filename): File|null
|
||||
{
|
||||
return Find::file($path, $filename);
|
||||
}
|
||||
|
@ -88,31 +79,26 @@ class Api extends BaseApi
|
|||
* Returns the model's object for the given path
|
||||
*
|
||||
* @param string $path Path to parent model
|
||||
* @return \Kirby\Cms\Model|null
|
||||
* @throws \Kirby\Exception\InvalidArgumentException if the model type is invalid
|
||||
* @throws \Kirby\Exception\NotFoundException if the model cannot be found
|
||||
*/
|
||||
public function parent(string $path)
|
||||
public function parent(string $path): Model|null
|
||||
{
|
||||
return Find::parent($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Kirby instance
|
||||
*
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
public function kirby(): App
|
||||
{
|
||||
return $this->kirby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language request header
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function language(): ?string
|
||||
public function language(): string|null
|
||||
{
|
||||
return $this->requestQuery('language') ?? $this->requestHeaders('x-language');
|
||||
}
|
||||
|
@ -121,10 +107,9 @@ class Api extends BaseApi
|
|||
* Returns the page object for the given id
|
||||
*
|
||||
* @param string $id Page's id
|
||||
* @return \Kirby\Cms\Page|null
|
||||
* @throws \Kirby\Exception\NotFoundException if the page cannot be found
|
||||
*/
|
||||
public function page(string $id)
|
||||
public function page(string $id): Page|null
|
||||
{
|
||||
return Find::page($id);
|
||||
}
|
||||
|
@ -133,39 +118,26 @@ class Api extends BaseApi
|
|||
* Returns the subpages for the given
|
||||
* parent. The subpages can be filtered
|
||||
* by status (draft, listed, unlisted, published, all)
|
||||
*
|
||||
* @param string|null $parentId
|
||||
* @param string|null $status
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function pages(string $parentId = null, string $status = null)
|
||||
public function pages(string|null $parentId = null, string|null $status = null): Pages
|
||||
{
|
||||
$parent = $parentId === null ? $this->site() : $this->page($parentId);
|
||||
|
||||
switch ($status) {
|
||||
case 'all':
|
||||
return $parent->childrenAndDrafts();
|
||||
case 'draft':
|
||||
case 'drafts':
|
||||
return $parent->drafts();
|
||||
case 'listed':
|
||||
return $parent->children()->listed();
|
||||
case 'unlisted':
|
||||
return $parent->children()->unlisted();
|
||||
case 'published':
|
||||
default:
|
||||
return $parent->children();
|
||||
}
|
||||
return match ($status) {
|
||||
'all' => $parent->childrenAndDrafts(),
|
||||
'draft', 'drafts' => $parent->drafts(),
|
||||
'listed' => $parent->children()->listed(),
|
||||
'unlisted' => $parent->children()->unlisted(),
|
||||
'published' => $parent->children(),
|
||||
default => $parent->children()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for direct subpages of the
|
||||
* given parent
|
||||
*
|
||||
* @param string|null $parent
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function searchPages(string $parent = null)
|
||||
public function searchPages(string|null $parent = null): Pages
|
||||
{
|
||||
$pages = $this->pages($parent, $this->requestQuery('status'));
|
||||
|
||||
|
@ -180,9 +152,8 @@ class Api extends BaseApi
|
|||
* Returns the current Session instance
|
||||
*
|
||||
* @param array $options Additional options, see the session component
|
||||
* @return \Kirby\Session\Session
|
||||
*/
|
||||
public function session(array $options = [])
|
||||
public function session(array $options = []): Session
|
||||
{
|
||||
return $this->kirby->session(array_merge([
|
||||
'detect' => true
|
||||
|
@ -192,10 +163,9 @@ class Api extends BaseApi
|
|||
/**
|
||||
* Setter for the parent Kirby instance
|
||||
*
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
* @return $this
|
||||
*/
|
||||
protected function setKirby(App $kirby)
|
||||
protected function setKirby(App $kirby): static
|
||||
{
|
||||
$this->kirby = $kirby;
|
||||
return $this;
|
||||
|
@ -203,10 +173,8 @@ class Api extends BaseApi
|
|||
|
||||
/**
|
||||
* Returns the site object
|
||||
*
|
||||
* @return \Kirby\Cms\Site
|
||||
*/
|
||||
public function site()
|
||||
public function site(): Site
|
||||
{
|
||||
return $this->kirby->site();
|
||||
}
|
||||
|
@ -217,10 +185,9 @@ class Api extends BaseApi
|
|||
* id is passed
|
||||
*
|
||||
* @param string|null $id User's id
|
||||
* @return \Kirby\Cms\User|null
|
||||
* @throws \Kirby\Exception\NotFoundException if the user for the given id cannot be found
|
||||
*/
|
||||
public function user(string $id = null)
|
||||
public function user(string|null $id = null): User|null
|
||||
{
|
||||
try {
|
||||
return Find::user($id);
|
||||
|
@ -235,10 +202,8 @@ class Api extends BaseApi
|
|||
|
||||
/**
|
||||
* Returns the users collection
|
||||
*
|
||||
* @return \Kirby\Cms\Users
|
||||
*/
|
||||
public function users()
|
||||
public function users(): Users
|
||||
{
|
||||
return $this->kirby->users();
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\ErrorPageException;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
|
@ -23,6 +25,7 @@ use Kirby\Toolkit\Config;
|
|||
use Kirby\Toolkit\Controller;
|
||||
use Kirby\Toolkit\Properties;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Uuid\Uuid;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
|
@ -380,7 +383,7 @@ class App
|
|||
*/
|
||||
public function collections()
|
||||
{
|
||||
return $this->collections = $this->collections ?? new Collections();
|
||||
return $this->collections ??= new Collections();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -435,7 +438,7 @@ class App
|
|||
|
||||
$salt = $this->option('content.salt', $default);
|
||||
|
||||
if (is_a($salt, 'Closure') === true) {
|
||||
if ($salt instanceof Closure) {
|
||||
$salt = $salt($model);
|
||||
}
|
||||
|
||||
|
@ -496,7 +499,11 @@ class App
|
|||
|
||||
// registry controller
|
||||
if ($controller = $this->extension('controllers', $name)) {
|
||||
return is_a($controller, 'Kirby\Toolkit\Controller') ? $controller : new Controller($controller);
|
||||
if ($controller instanceof Controller) {
|
||||
return $controller;
|
||||
}
|
||||
|
||||
return new Controller($controller);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -520,7 +527,7 @@ class App
|
|||
* @param string|null $check Pass a token here to compare it to the one in the session
|
||||
* @return string|bool Either the token or a boolean check result
|
||||
*/
|
||||
public function csrf(?string $check = null)
|
||||
public function csrf(string|null $check = null)
|
||||
{
|
||||
$session = $this->session();
|
||||
|
||||
|
@ -557,7 +564,7 @@ class App
|
|||
*/
|
||||
public function defaultLanguage()
|
||||
{
|
||||
return $this->defaultLanguage = $this->defaultLanguage ?? $this->languages()->default();
|
||||
return $this->defaultLanguage ??= $this->languages()->default();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -633,28 +640,26 @@ class App
|
|||
*/
|
||||
public function file(string $path, $parent = null, bool $drafts = true)
|
||||
{
|
||||
// find by global UUID
|
||||
if (Uuid::is($path, 'file') === true) {
|
||||
// prefer files of parent, when parent given
|
||||
return Uuid::for($path, $parent?->files())->model();
|
||||
}
|
||||
|
||||
$parent = $parent ?? $this->site();
|
||||
$id = dirname($path);
|
||||
$filename = basename($path);
|
||||
|
||||
if (is_a($parent, 'Kirby\Cms\User') === true) {
|
||||
if ($parent instanceof User) {
|
||||
return $parent->file($filename);
|
||||
}
|
||||
|
||||
if (is_a($parent, 'Kirby\Cms\File') === true) {
|
||||
if ($parent instanceof File) {
|
||||
$parent = $parent->parent();
|
||||
}
|
||||
|
||||
if ($id === '.') {
|
||||
if ($file = $parent->file($filename)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
if ($file = $this->site()->file($filename)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return null;
|
||||
return $parent->file($filename) ?? $this->site()->file($filename);
|
||||
}
|
||||
|
||||
if ($page = $this->page($id, $parent, $drafts)) {
|
||||
|
@ -680,7 +685,7 @@ class App
|
|||
*
|
||||
* @todo merge with App::file()
|
||||
*/
|
||||
public function image(?string $path = null)
|
||||
public function image(string|null $path = null)
|
||||
{
|
||||
if ($path === null) {
|
||||
return $this->site()->page()->image();
|
||||
|
@ -693,23 +698,13 @@ class App
|
|||
$uri = null;
|
||||
}
|
||||
|
||||
switch ($uri) {
|
||||
case '/':
|
||||
$parent = $this->site();
|
||||
break;
|
||||
case null:
|
||||
$parent = $this->site()->page();
|
||||
break;
|
||||
default:
|
||||
$parent = $this->site()->page($uri);
|
||||
break;
|
||||
}
|
||||
$parent = match ($uri) {
|
||||
'/' => $this->site(),
|
||||
null => $this->site()->page(),
|
||||
default => $this->site()->page($uri)
|
||||
};
|
||||
|
||||
if ($parent) {
|
||||
return $parent->image($filename);
|
||||
}
|
||||
|
||||
return null;
|
||||
return $parent?->image($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -718,18 +713,19 @@ class App
|
|||
* @param \Kirby\Cms\App|null $instance
|
||||
* @param bool $lazy If `true`, the instance is only returned if already existing
|
||||
* @return static|null
|
||||
* @psalm-return ($lazy is false ? static : static|null)
|
||||
*/
|
||||
public static function instance(self $instance = null, bool $lazy = false)
|
||||
{
|
||||
if ($instance === null) {
|
||||
if ($lazy === true) {
|
||||
return static::$instance;
|
||||
} else {
|
||||
return static::$instance ?? new static();
|
||||
}
|
||||
if ($instance !== null) {
|
||||
return static::$instance = $instance;
|
||||
}
|
||||
|
||||
return static::$instance = $instance;
|
||||
if ($lazy === true) {
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
return static::$instance ?? new static();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -746,8 +742,8 @@ class App
|
|||
$response = $this->response();
|
||||
|
||||
// any direct exception will be turned into an error page
|
||||
if (is_a($input, 'Throwable') === true) {
|
||||
if (is_a($input, 'Kirby\Exception\Exception') === true) {
|
||||
if ($input instanceof Throwable) {
|
||||
if ($input instanceof Exception) {
|
||||
$code = $input->getHttpCode();
|
||||
} else {
|
||||
$code = $input->getCode();
|
||||
|
@ -778,7 +774,7 @@ class App
|
|||
}
|
||||
|
||||
// (Modified) global response configuration, e.g. in routes
|
||||
if (is_a($input, 'Kirby\Cms\Responder') === true) {
|
||||
if ($input instanceof Responder) {
|
||||
// return the passed object unmodified (without injecting headers
|
||||
// from the global object) to allow a complete response override
|
||||
// https://github.com/getkirby/kirby/pull/4144#issuecomment-1034766726
|
||||
|
@ -786,37 +782,41 @@ class App
|
|||
}
|
||||
|
||||
// Responses
|
||||
if (is_a($input, 'Kirby\Http\Response') === true) {
|
||||
if ($input instanceof Response) {
|
||||
$data = $input->toArray();
|
||||
|
||||
// inject headers from the global response configuration
|
||||
// lazily (only if they are not already set);
|
||||
// the case-insensitive nature of headers will be
|
||||
// handled by PHP's `header()` function
|
||||
$data['headers'] = array_merge($response->headers(), $data['headers']);
|
||||
$data['headers'] = array_merge(
|
||||
$response->headers(),
|
||||
$data['headers']
|
||||
);
|
||||
|
||||
return new Response($data);
|
||||
}
|
||||
|
||||
// Pages
|
||||
if (is_a($input, 'Kirby\Cms\Page')) {
|
||||
if ($input instanceof Page) {
|
||||
try {
|
||||
$html = $input->render();
|
||||
} catch (ErrorPageException $e) {
|
||||
return $this->io($e);
|
||||
}
|
||||
|
||||
if ($input->isErrorPage() === true) {
|
||||
if ($response->code() === null) {
|
||||
$response->code(404);
|
||||
}
|
||||
if (
|
||||
$input->isErrorPage() === true &&
|
||||
$response->code() === null
|
||||
) {
|
||||
$response->code(404);
|
||||
}
|
||||
|
||||
return $response->send($html);
|
||||
}
|
||||
|
||||
// Files
|
||||
if (is_a($input, 'Kirby\Cms\File')) {
|
||||
if ($input instanceof File) {
|
||||
return $response->redirect($input->mediaUrl(), 307)->send();
|
||||
}
|
||||
|
||||
|
@ -844,7 +844,7 @@ class App
|
|||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function kirbytag($type, ?string $value = null, array $attr = [], array $data = []): string
|
||||
public function kirbytag($type, string|null $value = null, array $attr = [], array $data = []): string
|
||||
{
|
||||
if (is_array($type) === true) {
|
||||
$kirbytag = $type;
|
||||
|
@ -895,24 +895,13 @@ class App
|
|||
* @internal
|
||||
* @param string|null $text
|
||||
* @param array $options
|
||||
* @param bool $inline (deprecated: use $options['markdown']['inline'] instead)
|
||||
* @return string
|
||||
* @todo remove $inline parameter in in 3.8.0
|
||||
*/
|
||||
public function kirbytext(string $text = null, array $options = [], bool $inline = false): string
|
||||
public function kirbytext(string $text = null, array $options = []): string
|
||||
{
|
||||
// warning for deprecated fourth parameter
|
||||
// @codeCoverageIgnoreStart
|
||||
if (func_num_args() === 3) {
|
||||
Helpers::deprecated('Cms\App::kirbytext(): the $inline parameter is deprecated and will be removed in Kirby 3.8.0. Use $options[\'markdown\'][\'inline\'] instead.');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$options['markdown']['inline'] ??= $inline;
|
||||
|
||||
$text = $this->apply('kirbytext:before', compact('text'), 'text');
|
||||
$text = $this->kirbytags($text, $options);
|
||||
$text = $this->markdown($text, $options['markdown']);
|
||||
$text = $this->markdown($text, $options['markdown'] ?? []);
|
||||
|
||||
if ($this->option('smartypants', false) !== false) {
|
||||
$text = $this->smartypants($text);
|
||||
|
@ -936,14 +925,18 @@ class App
|
|||
}
|
||||
|
||||
if ($code === 'default') {
|
||||
return $this->languages()->default();
|
||||
return $this->defaultLanguage();
|
||||
}
|
||||
|
||||
// if requesting a non-default language,
|
||||
// find it but don't cache it
|
||||
if ($code !== null) {
|
||||
return $this->languages()->find($code);
|
||||
}
|
||||
|
||||
return $this->language = $this->language ?? $this->languages()->default();
|
||||
// otherwise return language set by `AppTranslation::setCurrentLanguage`
|
||||
// or default language
|
||||
return $this->language ??= $this->defaultLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -953,22 +946,15 @@ class App
|
|||
* @param string|null $languageCode
|
||||
* @return string|null
|
||||
*/
|
||||
public function languageCode(string $languageCode = null): ?string
|
||||
public function languageCode(string $languageCode = null): string|null
|
||||
{
|
||||
if ($language = $this->language($languageCode)) {
|
||||
return $language->code();
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->language($languageCode)?->code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available site languages
|
||||
*
|
||||
* @param bool
|
||||
* @return \Kirby\Cms\Languages
|
||||
*/
|
||||
public function languages(bool $clone = true)
|
||||
public function languages(bool $clone = true): Languages
|
||||
{
|
||||
if ($this->languages !== null) {
|
||||
return $clone === true ? clone $this->languages : $this->languages;
|
||||
|
@ -1006,34 +992,18 @@ class App
|
|||
*
|
||||
* @internal
|
||||
* @param string|null $text
|
||||
* @param bool|array $options Boolean inline value is deprecated, use `['inline' => true]` instead
|
||||
* @param array $options
|
||||
* @return string
|
||||
* @todo remove boolean $options in in 3.8.0
|
||||
*/
|
||||
public function markdown(string $text = null, $options = null): string
|
||||
public function markdown(string $text = null, array $options = null): string
|
||||
{
|
||||
// support for the old syntax to enable inline mode as second argument
|
||||
// @codeCoverageIgnoreStart
|
||||
if (is_bool($options) === true) {
|
||||
Helpers::deprecated('Cms\App::markdown(): Passing a boolean as second parameter has been deprecated and won\'t be supported anymore in Kirby 3.8.0. Instead pass array with the key "inline" set to true or false.');
|
||||
|
||||
$options = [
|
||||
'inline' => $options
|
||||
];
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
// merge global options with local options
|
||||
$options = array_merge(
|
||||
$this->options['markdown'] ?? [],
|
||||
(array)$options
|
||||
);
|
||||
|
||||
// TODO: remove passing the $inline parameter in 3.8.0
|
||||
// $options['inline'] is set to `false` to avoid the deprecation
|
||||
// warning in the component; this can also be removed in 3.8.0
|
||||
$inline = $options['inline'] ??= false;
|
||||
return ($this->component('markdown'))($this, $text, $options, $inline);
|
||||
return ($this->component('markdown'))($this, $text, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1059,7 +1029,7 @@ class App
|
|||
*/
|
||||
public function nonce(): string
|
||||
{
|
||||
return $this->nonce = $this->nonce ?? base64_encode(random_bytes(20));
|
||||
return $this->nonce ??= base64_encode(random_bytes(20));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1096,7 +1066,7 @@ class App
|
|||
|
||||
// load the main config options
|
||||
$root = $this->root('config');
|
||||
$options = F::load($root . '/config.php', []);
|
||||
$options = F::load($root . '/config.php', [], allowOutput: false);
|
||||
|
||||
// merge into one clean options array
|
||||
return $this->options = array_replace_recursive(Config::$data, $options);
|
||||
|
@ -1111,19 +1081,27 @@ class App
|
|||
*/
|
||||
protected function optionsFromEnvironment(array $props = []): array
|
||||
{
|
||||
$globalUrl = $this->options['url'] ?? null;
|
||||
$root = $this->root('config');
|
||||
|
||||
// create the environment based on the URL setup
|
||||
// first load `config/env.php` to access its `url` option
|
||||
$envOptions = F::load($root . '/env.php', [], allowOutput: false);
|
||||
|
||||
// use the option from the main `config.php`,
|
||||
// but allow the `env.php` to override it
|
||||
$globalUrl = $envOptions['url'] ?? $this->options['url'] ?? null;
|
||||
|
||||
// create the URL setup based on hostname and server IP address
|
||||
$this->environment = new Environment([
|
||||
'allowed' => $globalUrl,
|
||||
'cli' => $props['cli'] ?? null,
|
||||
], $props['server'] ?? null);
|
||||
|
||||
// merge into one clean options array
|
||||
$options = $this->environment()->options($this->root('config'));
|
||||
$this->options = array_replace_recursive($this->options, $options);
|
||||
// merge into one clean options array;
|
||||
// the `env.php` options always override everything else
|
||||
$hostAddrOptions = $this->environment()->options($root);
|
||||
$this->options = array_replace_recursive($this->options, $hostAddrOptions, $envOptions);
|
||||
|
||||
// reload the environment if the environment config has overridden
|
||||
// reload the environment if the host/address config has overridden
|
||||
// the `url` option; this ensures that the base URL is correct
|
||||
$envUrl = $this->options['url'] ?? null;
|
||||
if ($envUrl !== $globalUrl) {
|
||||
|
@ -1201,12 +1179,17 @@ class App
|
|||
* @param bool $drafts
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function page(?string $id = null, $parent = null, bool $drafts = true)
|
||||
public function page(string|null $id = null, $parent = null, bool $drafts = true)
|
||||
{
|
||||
if ($id === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// find by global UUID
|
||||
if (Uuid::is($id, 'page') === true) {
|
||||
return Uuid::for($id, $parent?->childrenAndDrafts())->model();
|
||||
}
|
||||
|
||||
$parent = $parent ?? $this->site();
|
||||
|
||||
if ($page = $parent->find($id)) {
|
||||
|
@ -1252,6 +1235,10 @@ class App
|
|||
*/
|
||||
public function render(string $path = null, string $method = null)
|
||||
{
|
||||
if (($_ENV['KIRBY_RENDER'] ?? true) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->io($this->call($path, $method));
|
||||
}
|
||||
|
||||
|
@ -1308,7 +1295,10 @@ class App
|
|||
|
||||
// search for a draft if the page cannot be found
|
||||
if (!$page && $draft = $site->draft($path)) {
|
||||
if ($this->user() || $draft->isVerified($this->request()->get('token'))) {
|
||||
if (
|
||||
$this->user() ||
|
||||
$draft->isVerified($this->request()->get('token'))
|
||||
) {
|
||||
$page = $draft;
|
||||
}
|
||||
}
|
||||
|
@ -1335,7 +1325,7 @@ class App
|
|||
}
|
||||
|
||||
return $response->body($output);
|
||||
} catch (NotFoundException $e) {
|
||||
} catch (NotFoundException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1359,7 +1349,7 @@ class App
|
|||
*/
|
||||
public function response()
|
||||
{
|
||||
return $this->response = $this->response ?? new Responder();
|
||||
return $this->response ??= new Responder();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1369,7 +1359,7 @@ class App
|
|||
*/
|
||||
public function roles()
|
||||
{
|
||||
return $this->roles = $this->roles ?? Roles::load($this->root('roles'));
|
||||
return $this->roles ??= Roles::load($this->root('roles'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1378,7 +1368,7 @@ class App
|
|||
* @param string $type
|
||||
* @return string|null
|
||||
*/
|
||||
public function root(string $type = 'index'): ?string
|
||||
public function root(string $type = 'index'): string|null
|
||||
{
|
||||
return $this->roots->__get($type);
|
||||
}
|
||||
|
@ -1572,12 +1562,16 @@ class App
|
|||
* @deprecated 3.7.0 Use `$kirby->environment()` instead
|
||||
*
|
||||
* @return \Kirby\Http\Environment
|
||||
* @todo Start throwing deprecation warnings in 3.8.0
|
||||
* @deprecated Will be removed in Kirby 3.9.0
|
||||
* @todo Remove in 3.9.0
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function server()
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
Helpers::deprecated('$kirby->server() has been deprecated and will be removed in Kirby 3.9.0. Use $kirby->environment() instead.');
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return $this->environment();
|
||||
}
|
||||
|
||||
|
@ -1588,7 +1582,7 @@ class App
|
|||
*/
|
||||
public function site()
|
||||
{
|
||||
return $this->site = $this->site ?? new Site([
|
||||
return $this->site ??= new Site([
|
||||
'errorPageId' => $this->options['error'] ?? 'error',
|
||||
'homePageId' => $this->options['home'] ?? 'home',
|
||||
'kirby' => $this,
|
||||
|
@ -1609,7 +1603,9 @@ class App
|
|||
|
||||
if ($options === false) {
|
||||
return $text;
|
||||
} elseif (is_array($options) === false) {
|
||||
}
|
||||
|
||||
if (is_array($options) === false) {
|
||||
$options = [];
|
||||
}
|
||||
|
||||
|
@ -1628,13 +1624,13 @@ class App
|
|||
* Uses the snippet component to create
|
||||
* and return a template snippet
|
||||
*
|
||||
* @internal
|
||||
* @param mixed $name
|
||||
* @param array|object $data Variables or an object that becomes `$item`
|
||||
* @param bool $return On `false`, directly echo the snippet
|
||||
* @return string|null
|
||||
* @psalm-return ($return is true ? string : null)
|
||||
*/
|
||||
public function snippet($name, $data = [], bool $return = true): ?string
|
||||
public function snippet($name, $data = [], bool $return = true): string|null
|
||||
{
|
||||
if (is_object($data) === true) {
|
||||
$data = ['item' => $data];
|
||||
|
@ -1657,7 +1653,7 @@ class App
|
|||
*/
|
||||
public function system()
|
||||
{
|
||||
return $this->system = $this->system ?? new System($this);
|
||||
return $this->system ??= new System($this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1740,6 +1736,7 @@ class App
|
|||
* @param string $type
|
||||
* @param bool $object If set to `true`, the URL is converted to an object
|
||||
* @return string|\Kirby\Http\Uri|null
|
||||
* @psalm-return ($object is false ? string|null : \Kirby\Http\Uri)
|
||||
*/
|
||||
public function url(string $type = 'index', bool $object = false)
|
||||
{
|
||||
|
@ -1777,11 +1774,11 @@ class App
|
|||
* @return string|null
|
||||
* @throws \Kirby\Exception\LogicException if the Kirby version cannot be detected
|
||||
*/
|
||||
public static function version(): ?string
|
||||
public static function version(): string|null
|
||||
{
|
||||
try {
|
||||
return static::$version = static::$version ?? Data::read(dirname(__DIR__, 2) . '/composer.json')['version'] ?? null;
|
||||
} catch (Throwable $e) {
|
||||
return static::$version ??= Data::read(dirname(__DIR__, 2) . '/composer.json')['version'] ?? null;
|
||||
} catch (Throwable) {
|
||||
throw new LogicException('The Kirby version cannot be detected. The composer.json is probably missing or not readable.');
|
||||
}
|
||||
}
|
||||
|
@ -1803,6 +1800,6 @@ class App
|
|||
*/
|
||||
public function visitor()
|
||||
{
|
||||
return $this->visitor = $this->visitor ?? new Visitor();
|
||||
return $this->visitor ??= new Visitor();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Cache\Cache;
|
||||
use Kirby\Cache\NullCache;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
|
@ -54,7 +55,7 @@ trait AppCaches
|
|||
$cache = new $className($options);
|
||||
|
||||
// check if it is a usable cache object
|
||||
if (is_a($cache, 'Kirby\Cache\Cache') !== true) {
|
||||
if ($cache instanceof Cache === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'app.invalid.cacheType',
|
||||
'data' => ['type' => $type]
|
||||
|
@ -72,7 +73,8 @@ trait AppCaches
|
|||
*/
|
||||
protected function cacheOptions(string $key): array
|
||||
{
|
||||
$options = $this->option($this->cacheOptionsKey($key), false);
|
||||
$options = $this->option($this->cacheOptionsKey($key), null);
|
||||
$options ??= $this->core()->caches()[$key] ?? false;
|
||||
|
||||
if ($options === false) {
|
||||
return [
|
||||
|
@ -94,9 +96,9 @@ trait AppCaches
|
|||
|
||||
if ($options === true) {
|
||||
return $defaults;
|
||||
} else {
|
||||
return array_merge($defaults, $options);
|
||||
}
|
||||
|
||||
return array_merge($defaults, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Throwable;
|
||||
use Whoops\Handler\CallbackHandler;
|
||||
use Whoops\Handler\Handler;
|
||||
use Whoops\Handler\PlainTextHandler;
|
||||
|
@ -84,7 +87,7 @@ trait AppErrors
|
|||
$handler = new CallbackHandler(function ($exception, $inspector, $run) {
|
||||
$fatal = $this->option('fatal');
|
||||
|
||||
if (is_a($fatal, 'Closure') === true) {
|
||||
if ($fatal instanceof Closure) {
|
||||
echo $fatal($this, $exception);
|
||||
} else {
|
||||
include $this->root('kirby') . '/views/fatal.php';
|
||||
|
@ -109,11 +112,11 @@ trait AppErrors
|
|||
protected function handleJsonErrors(): void
|
||||
{
|
||||
$handler = new CallbackHandler(function ($exception, $inspector, $run) {
|
||||
if (is_a($exception, 'Kirby\Exception\Exception') === true) {
|
||||
if ($exception instanceof Exception) {
|
||||
$httpCode = $exception->getHttpCode();
|
||||
$code = $exception->getCode();
|
||||
$details = $exception->getDetails();
|
||||
} elseif (is_a($exception, '\Throwable') === true) {
|
||||
} elseif ($exception instanceof Throwable) {
|
||||
$httpCode = 500;
|
||||
$code = $exception->getCode();
|
||||
$details = null;
|
||||
|
@ -160,19 +163,19 @@ trait AppErrors
|
|||
$whoops = $this->whoops();
|
||||
$whoops->clearHandlers();
|
||||
$whoops->pushHandler($handler);
|
||||
$whoops->pushHandler($this->getExceptionHookWhoopsHandler());
|
||||
$whoops->pushHandler($this->getAdditionalWhoopsHandler());
|
||||
$whoops->register(); // will only do something if not already registered
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a callback handler for triggering the `system.exception` hook
|
||||
*
|
||||
* @return \Whoops\Handler\CallbackHandler
|
||||
* Whoops callback handler for additional error handling
|
||||
* (`system.exception` hook and output to error log)
|
||||
*/
|
||||
protected function getExceptionHookWhoopsHandler(): CallbackHandler
|
||||
protected function getAdditionalWhoopsHandler(): CallbackHandler
|
||||
{
|
||||
return new CallbackHandler(function ($exception, $inspector, $run) {
|
||||
$this->trigger('system.exception', compact('exception'));
|
||||
error_log($exception);
|
||||
return Handler::DONE;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ trait AppPlugins
|
|||
'blueprints' => [],
|
||||
'cacheTypes' => [],
|
||||
'collections' => [],
|
||||
'commands' => [],
|
||||
'components' => [],
|
||||
'controllers' => [],
|
||||
'collectionFilters' => [],
|
||||
|
@ -120,14 +121,14 @@ trait AppPlugins
|
|||
protected function extendApi($api): array
|
||||
{
|
||||
if (is_array($api) === true) {
|
||||
if (is_a($api['routes'] ?? [], 'Closure') === true) {
|
||||
if (($api['routes'] ?? []) instanceof Closure) {
|
||||
$api['routes'] = $api['routes']($this);
|
||||
}
|
||||
|
||||
return $this->extensions['api'] = A::merge($this->extensions['api'], $api, A::MERGE_APPEND);
|
||||
} else {
|
||||
return $this->extensions['api'];
|
||||
}
|
||||
|
||||
return $this->extensions['api'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -139,10 +140,7 @@ trait AppPlugins
|
|||
protected function extendAreas(array $areas): array
|
||||
{
|
||||
foreach ($areas as $id => $area) {
|
||||
if (isset($this->extensions['areas'][$id]) === false) {
|
||||
$this->extensions['areas'][$id] = [];
|
||||
}
|
||||
|
||||
$this->extensions['areas'][$id] ??= [];
|
||||
$this->extensions['areas'][$id][] = $area;
|
||||
}
|
||||
|
||||
|
@ -215,6 +213,17 @@ trait AppPlugins
|
|||
return $this->extensions['cacheTypes'] = array_merge($this->extensions['cacheTypes'], $cacheTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional CLI commands
|
||||
*
|
||||
* @param array $commands
|
||||
* @return array
|
||||
*/
|
||||
protected function extendCommands(array $commands): array
|
||||
{
|
||||
return $this->extensions['commands'] = array_merge($this->extensions['commands'], $commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional collection filters
|
||||
*
|
||||
|
@ -376,9 +385,7 @@ trait AppPlugins
|
|||
protected function extendHooks(array $hooks): array
|
||||
{
|
||||
foreach ($hooks as $name => $callbacks) {
|
||||
if (isset($this->extensions['hooks'][$name]) === false) {
|
||||
$this->extensions['hooks'][$name] = [];
|
||||
}
|
||||
$this->extensions['hooks'][$name] ??= [];
|
||||
|
||||
if (is_array($callbacks) === false) {
|
||||
$callbacks = [$callbacks];
|
||||
|
@ -520,7 +527,7 @@ trait AppPlugins
|
|||
*/
|
||||
protected function extendRoutes($routes): array
|
||||
{
|
||||
if (is_a($routes, 'Closure') === true) {
|
||||
if ($routes instanceof Closure) {
|
||||
$routes = $routes($this);
|
||||
}
|
||||
|
||||
|
@ -705,7 +712,7 @@ trait AppPlugins
|
|||
$class = str_replace(['.', '-', '_'], '', $name) . 'Page';
|
||||
|
||||
// load the model class
|
||||
F::loadOnce($model);
|
||||
F::loadOnce($model, allowOutput: false);
|
||||
|
||||
if (class_exists($class) === true) {
|
||||
$models[$name] = $class;
|
||||
|
@ -896,7 +903,7 @@ trait AppPlugins
|
|||
$styles = $dir . '/index.css';
|
||||
|
||||
if (is_file($entry) === true) {
|
||||
F::loadOnce($entry);
|
||||
F::loadOnce($entry, allowOutput: false);
|
||||
} elseif (is_file($script) === true || is_file($styles) === true) {
|
||||
// if no PHP file is present but an index.js or index.css,
|
||||
// register as anonymous plugin (without actual extensions)
|
||||
|
|
|
@ -27,11 +27,7 @@ trait AppTranslations
|
|||
protected function i18n(): void
|
||||
{
|
||||
I18n::$load = function ($locale): array {
|
||||
$data = [];
|
||||
|
||||
if ($translation = $this->translation($locale)) {
|
||||
$data = $translation->data();
|
||||
}
|
||||
$data = $this->translation($locale)?->data() ?? [];
|
||||
|
||||
// inject translations from the current language
|
||||
if (
|
||||
|
@ -49,9 +45,9 @@ trait AppTranslations
|
|||
I18n::$locale = function (): string {
|
||||
if ($this->multilang() === true) {
|
||||
return $this->defaultLanguage()->code();
|
||||
} else {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
return 'en';
|
||||
};
|
||||
|
||||
I18n::$fallback = function (): array {
|
||||
|
@ -71,9 +67,9 @@ trait AppTranslations
|
|||
$fallback[] = 'en';
|
||||
|
||||
return $fallback;
|
||||
} else {
|
||||
return ['en'];
|
||||
}
|
||||
|
||||
return ['en'];
|
||||
};
|
||||
|
||||
I18n::$translations = [];
|
||||
|
@ -127,11 +123,8 @@ trait AppTranslations
|
|||
return $this->language = null;
|
||||
}
|
||||
|
||||
if ($language = $this->language($languageCode)) {
|
||||
$this->language = $language;
|
||||
} else {
|
||||
$this->language = $this->defaultLanguage();
|
||||
}
|
||||
$this->language = $this->language($languageCode);
|
||||
$this->language ??= $this->defaultLanguage();
|
||||
|
||||
if ($this->language) {
|
||||
Locale::set($this->language->locale());
|
||||
|
@ -161,13 +154,13 @@ trait AppTranslations
|
|||
* @param string|null $locale Locale name or `null` for the current locale
|
||||
* @return \Kirby\Cms\Translation
|
||||
*/
|
||||
public function translation(?string $locale = null)
|
||||
public function translation(string|null $locale = null)
|
||||
{
|
||||
$locale = $locale ?? I18n::locale();
|
||||
$locale = basename($locale);
|
||||
|
||||
// prefer loading them from the translations collection
|
||||
if (is_a($this->translations, 'Kirby\Cms\Translations') === true) {
|
||||
if ($this->translations instanceof Translations) {
|
||||
if ($translation = $this->translations()->find($locale)) {
|
||||
return $translation;
|
||||
}
|
||||
|
@ -192,7 +185,7 @@ trait AppTranslations
|
|||
*/
|
||||
public function translations()
|
||||
{
|
||||
if (is_a($this->translations, 'Kirby\Cms\Translations') === true) {
|
||||
if ($this->translations instanceof Translations) {
|
||||
return $this->translations;
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ trait AppUsers
|
|||
*/
|
||||
public function auth()
|
||||
{
|
||||
return $this->auth = $this->auth ?? new Auth($this);
|
||||
return $this->auth ??= new Auth($this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -48,7 +48,7 @@ trait AppUsers
|
|||
* if called with callback: Return value from the callback
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function impersonate(?string $who = null, ?Closure $callback = null)
|
||||
public function impersonate(string|null $who = null, Closure|null $callback = null)
|
||||
{
|
||||
$auth = $this->auth();
|
||||
|
||||
|
@ -60,8 +60,10 @@ trait AppUsers
|
|||
}
|
||||
|
||||
try {
|
||||
// bind the App object to the callback
|
||||
return $callback->call($this, $userAfter);
|
||||
// TODO: switch over in 3.9.0 to
|
||||
// return $callback($userAfter);
|
||||
$proxy = new AppUsersImpersonateProxy($this);
|
||||
return $callback->call($proxy, $userAfter);
|
||||
} catch (Throwable $e) {
|
||||
throw $e;
|
||||
} finally {
|
||||
|
@ -110,7 +112,7 @@ trait AppUsers
|
|||
* (when `$id` is passed as `null`)
|
||||
* @return \Kirby\Cms\User|null
|
||||
*/
|
||||
public function user(?string $id = null, bool $allowImpersonation = true)
|
||||
public function user(string|null $id = null, bool $allowImpersonation = true)
|
||||
{
|
||||
if ($id !== null) {
|
||||
return $this->users()->find($id);
|
||||
|
@ -118,12 +120,12 @@ trait AppUsers
|
|||
|
||||
if ($allowImpersonation === true && is_string($this->user) === true) {
|
||||
return $this->auth()->impersonate($this->user);
|
||||
} else {
|
||||
try {
|
||||
return $this->auth()->user(null, $allowImpersonation);
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->auth()->user(null, $allowImpersonation);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -134,7 +136,7 @@ trait AppUsers
|
|||
*/
|
||||
public function users()
|
||||
{
|
||||
if (is_a($this->users, 'Kirby\Cms\Users') === true) {
|
||||
if ($this->users instanceof Users) {
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
|
|
32
kirby/src/Cms/AppUsersImpersonateProxy.php
Normal file
32
kirby/src/Cms/AppUsersImpersonateProxy.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Temporary proxy class to ease transition
|
||||
* of binding the callback for `$kirby->impersonate()`
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Nico Hoffmann <nico@getkirby.com>,
|
||||
* Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*
|
||||
* @internal
|
||||
* @deprecated Will be removed in Kirby 3.9.0
|
||||
* @todo remove in 3.9.0
|
||||
*/
|
||||
class AppUsersImpersonateProxy
|
||||
{
|
||||
public function __construct(protected App $app)
|
||||
{
|
||||
}
|
||||
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
Helpers::deprecated('Calling $kirby->' . $name . '() as $this->' . $name . '() has been deprecated inside the $kirby->impersonate() callback function. Use a dedicated $kirby object for your call instead of $this. In Kirby 3.9.0 $this will no longer refer to the $kirby object, but the current context of the callback function.');
|
||||
|
||||
return $this->app->$name(...$arguments);
|
||||
}
|
||||
}
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Cms\Auth\Challenge;
|
||||
use Kirby\Cms\Auth\Status;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
|
@ -11,6 +13,7 @@ use Kirby\Exception\PermissionException;
|
|||
use Kirby\Filesystem\F;
|
||||
use Kirby\Http\Idn;
|
||||
use Kirby\Http\Request\Auth\BasicAuth;
|
||||
use Kirby\Session\Session;
|
||||
use Kirby\Toolkit\A;
|
||||
use Throwable;
|
||||
|
||||
|
@ -95,26 +98,44 @@ class Auth
|
|||
*/
|
||||
public function createChallenge(string $email, bool $long = false, string $mode = 'login')
|
||||
{
|
||||
$email = $this->validateEmail($email);
|
||||
|
||||
// rate-limit the number of challenges for DoS/DDoS protection
|
||||
$this->track($email, false);
|
||||
$email = Idn::decodeEmail($email);
|
||||
|
||||
$session = $this->kirby->session([
|
||||
'createMode' => 'cookie',
|
||||
'long' => $long === true
|
||||
]);
|
||||
|
||||
$challenge = null;
|
||||
if ($user = $this->kirby->users()->find($email)) {
|
||||
$timeout = $this->kirby->option('auth.challenge.timeout', 10 * 60);
|
||||
$timeout = $this->kirby->option('auth.challenge.timeout', 10 * 60);
|
||||
|
||||
// catch every exception to hide them from attackers
|
||||
// unless auth debugging is enabled
|
||||
try {
|
||||
$this->checkRateLimit($email);
|
||||
|
||||
// rate-limit the number of challenges for DoS/DDoS protection
|
||||
$this->track($email, false);
|
||||
|
||||
// try to find the provided user
|
||||
$user = $this->kirby->users()->find($email);
|
||||
if ($user === null) {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $email
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// try to find an enabled challenge that is available for that user
|
||||
$challenge = null;
|
||||
foreach ($this->enabledChallenges() as $name) {
|
||||
$class = static::$challenges[$name] ?? null;
|
||||
if (
|
||||
$class &&
|
||||
class_exists($class) === true &&
|
||||
is_subclass_of($class, 'Kirby\Cms\Auth\Challenge') === true &&
|
||||
is_subclass_of($class, Challenge::class) === true &&
|
||||
$class::isAvailable($user, $mode) === true
|
||||
) {
|
||||
$challenge = $name;
|
||||
|
@ -124,40 +145,30 @@ class Auth
|
|||
|
||||
if ($code !== null) {
|
||||
$session->set('kirby.challenge.code', password_hash($code, PASSWORD_DEFAULT));
|
||||
$session->set('kirby.challenge.timeout', time() + $timeout);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if no suitable challenge was found, `$challenge === null` at this point;
|
||||
// only leak this in debug mode
|
||||
if ($challenge === null && $this->kirby->option('debug') === true) {
|
||||
// if no suitable challenge was found, `$challenge === null` at this point
|
||||
if ($challenge === null) {
|
||||
throw new LogicException('Could not find a suitable authentication challenge');
|
||||
}
|
||||
} else {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
|
||||
// only leak the non-existing user in debug mode
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $email
|
||||
]
|
||||
]);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// only throw the exception in auth debug mode
|
||||
$this->fail($e);
|
||||
}
|
||||
|
||||
// always set the email, even if the challenge won't be
|
||||
// created to avoid leaking whether the user exists
|
||||
// always set the email and timeout, even if the challenge
|
||||
// won't be created; this avoids leaking whether the user exists
|
||||
$session->set('kirby.challenge.email', $email);
|
||||
$session->set('kirby.challenge.timeout', time() + $timeout);
|
||||
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder and to
|
||||
// avoid leaking whether the user exists
|
||||
usleep(random_int(1000, 300000));
|
||||
usleep(random_int(50000, 300000));
|
||||
|
||||
// clear the status cache
|
||||
$this->status = null;
|
||||
|
@ -303,33 +314,25 @@ class Auth
|
|||
* @return \Kirby\Cms\User|null
|
||||
* @throws \Kirby\Exception\NotFoundException if the given user cannot be found
|
||||
*/
|
||||
public function impersonate(?string $who = null)
|
||||
public function impersonate(string|null $who = null)
|
||||
{
|
||||
// clear the status cache
|
||||
$this->status = null;
|
||||
|
||||
switch ($who) {
|
||||
case null:
|
||||
return $this->impersonate = null;
|
||||
case 'kirby':
|
||||
return $this->impersonate = new User([
|
||||
'email' => 'kirby@getkirby.com',
|
||||
'id' => 'kirby',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
case 'nobody':
|
||||
return $this->impersonate = new User([
|
||||
'email' => 'nobody@getkirby.com',
|
||||
'id' => 'nobody',
|
||||
'role' => 'nobody',
|
||||
]);
|
||||
default:
|
||||
if ($user = $this->kirby->users()->find($who)) {
|
||||
return $this->impersonate = $user;
|
||||
}
|
||||
|
||||
throw new NotFoundException('The user "' . $who . '" cannot be found');
|
||||
}
|
||||
return $this->impersonate = match ($who) {
|
||||
null => null,
|
||||
'kirby' => new User([
|
||||
'email' => 'kirby@getkirby.com',
|
||||
'id' => 'kirby',
|
||||
'role' => 'admin',
|
||||
]),
|
||||
'nobody' => new User([
|
||||
'email' => 'nobody@getkirby.com',
|
||||
'id' => 'nobody',
|
||||
'role' => 'nobody',
|
||||
]),
|
||||
default => ($this->kirby->users()->find($who) ?? throw new NotFoundException('The user "' . $who . '" cannot be found'))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -492,34 +495,21 @@ class Auth
|
|||
}
|
||||
|
||||
/**
|
||||
* Ensures that email addresses with IDN domains are in Unicode format
|
||||
* and that the rate limit was not exceeded
|
||||
*
|
||||
* @param string $email
|
||||
* @return string The normalized Unicode email address
|
||||
* Ensures that the rate limit was not exceeded
|
||||
*
|
||||
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded
|
||||
*/
|
||||
protected function validateEmail(string $email): string
|
||||
protected function checkRateLimit(string $email): void
|
||||
{
|
||||
// ensure that email addresses with IDN domains are in Unicode format
|
||||
$email = Idn::decodeEmail($email);
|
||||
|
||||
// check for blocked ips
|
||||
if ($this->isBlocked($email) === true) {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
$message = 'Rate limit exceeded';
|
||||
} else {
|
||||
// avoid leaking security-relevant information
|
||||
$message = ['key' => 'access.login'];
|
||||
}
|
||||
|
||||
throw new PermissionException($message);
|
||||
throw new PermissionException([
|
||||
'details' => ['reason' => 'rate-limited'],
|
||||
'fallback' => 'Rate limit exceeded'
|
||||
]);
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -536,10 +526,12 @@ class Auth
|
|||
*/
|
||||
public function validatePassword(string $email, string $password)
|
||||
{
|
||||
$email = $this->validateEmail($email);
|
||||
$email = Idn::decodeEmail($email);
|
||||
|
||||
// validate the user
|
||||
try {
|
||||
$this->checkRateLimit($email);
|
||||
|
||||
// validate the user and its password
|
||||
if ($user = $this->kirby->users()->find($email)) {
|
||||
if ($user->validatePassword($password) === true) {
|
||||
return $user;
|
||||
|
@ -553,20 +545,25 @@ class Auth
|
|||
]
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
// log invalid login trial
|
||||
$this->track($email);
|
||||
$details = $e instanceof Exception ? $e->getDetails() : [];
|
||||
|
||||
// log invalid login trial unless the rate limit is already active
|
||||
if (($details['reason'] ?? null) !== 'rate-limited') {
|
||||
try {
|
||||
$this->track($email);
|
||||
} catch (Throwable $e) {
|
||||
// $e is overwritten with the exception
|
||||
// from the track method if there's one
|
||||
}
|
||||
}
|
||||
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder
|
||||
usleep(random_int(1000, 2000000));
|
||||
usleep(random_int(10000, 2000000));
|
||||
|
||||
// keep throwing the original error in debug mode,
|
||||
// otherwise hide it to avoid leaking security-relevant information
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
throw $e;
|
||||
} else {
|
||||
throw new PermissionException(['key' => 'access.login']);
|
||||
}
|
||||
$this->fail($e, new PermissionException(['key' => 'access.login']));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -590,7 +587,7 @@ class Auth
|
|||
try {
|
||||
$log = Data::read($this->logfile(), 'json');
|
||||
$read = true;
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
$log = [];
|
||||
$read = false;
|
||||
}
|
||||
|
@ -636,9 +633,7 @@ class Auth
|
|||
$this->impersonate = null;
|
||||
|
||||
// logout the current user if it exists
|
||||
if ($user = $this->user()) {
|
||||
$user->logout();
|
||||
}
|
||||
$this->user()?->logout();
|
||||
|
||||
// clear the pending challenge
|
||||
$session = $this->kirby->session();
|
||||
|
@ -671,7 +666,7 @@ class Auth
|
|||
* @param bool $triggerHook If `false`, no user.login:failed hook is triggered
|
||||
* @return bool
|
||||
*/
|
||||
public function track(?string $email, bool $triggerHook = true): bool
|
||||
public function track(string|null $email, bool $triggerHook = true): bool
|
||||
{
|
||||
if ($triggerHook === true) {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
|
@ -721,15 +716,25 @@ class Auth
|
|||
public function type(bool $allowImpersonation = true): string
|
||||
{
|
||||
$basicAuth = $this->kirby->option('api.basicAuth', false);
|
||||
$auth = $this->kirby->request()->auth();
|
||||
$request = $this->kirby->request();
|
||||
|
||||
if ($basicAuth === true && $auth && $auth->type() === 'basic') {
|
||||
if (
|
||||
$basicAuth === true &&
|
||||
|
||||
// only get the auth object if the option is enabled
|
||||
// to avoid triggering `$responder->usesAuth()` if
|
||||
// the option is disabled
|
||||
$request->auth() &&
|
||||
$request->auth()->type() === 'basic'
|
||||
) {
|
||||
return 'basic';
|
||||
} elseif ($allowImpersonation === true && $this->impersonate !== null) {
|
||||
return 'impersonate';
|
||||
} else {
|
||||
return 'session';
|
||||
}
|
||||
|
||||
if ($allowImpersonation === true && $this->impersonate !== null) {
|
||||
return 'impersonate';
|
||||
}
|
||||
|
||||
return 'session';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -756,16 +761,18 @@ class Auth
|
|||
}
|
||||
|
||||
return null;
|
||||
} elseif ($this->user !== false) {
|
||||
}
|
||||
|
||||
if ($this->user !== false) {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->type() === 'basic') {
|
||||
return $this->user = $this->currentUserFromBasicAuth();
|
||||
} else {
|
||||
return $this->user = $this->currentUserFromSession($session);
|
||||
}
|
||||
|
||||
return $this->user = $this->currentUserFromSession($session);
|
||||
} catch (Throwable $e) {
|
||||
$this->user = null;
|
||||
|
||||
|
@ -796,11 +803,35 @@ class Auth
|
|||
try {
|
||||
$session = $this->kirby->session();
|
||||
|
||||
// first check if we have an active challenge at all
|
||||
// time-limiting; check this early so that we can destroy the session no
|
||||
// matter if the user exists (avoids leaking user information to attackers)
|
||||
$timeout = $session->get('kirby.challenge.timeout');
|
||||
if ($timeout !== null && time() > $timeout) {
|
||||
// this challenge can never be completed,
|
||||
// so delete it immediately
|
||||
$this->logout();
|
||||
|
||||
throw new PermissionException([
|
||||
'details' => ['challengeDestroyed' => true],
|
||||
'fallback' => 'Authentication challenge timeout'
|
||||
]);
|
||||
}
|
||||
|
||||
// check if we have an active challenge
|
||||
$email = $session->get('kirby.challenge.email');
|
||||
$challenge = $session->get('kirby.challenge.type');
|
||||
if (is_string($email) !== true || is_string($challenge) !== true) {
|
||||
throw new InvalidArgumentException('No authentication challenge is active');
|
||||
// if the challenge timed out on the previous request, the
|
||||
// challenge data was already deleted from the session, so we can
|
||||
// set `challengeDestroyed` to `true` in this response as well;
|
||||
// however we must only base this on the email, not the type
|
||||
// (otherwise "faked" challenges would be leaked)
|
||||
$challengeDestroyed = is_string($email) !== true;
|
||||
|
||||
throw new InvalidArgumentException([
|
||||
'details' => compact('challengeDestroyed'),
|
||||
'fallback' => 'No authentication challenge is active'
|
||||
]);
|
||||
}
|
||||
|
||||
$user = $this->kirby->users()->find($email);
|
||||
|
@ -814,21 +845,12 @@ class Auth
|
|||
}
|
||||
|
||||
// rate-limiting
|
||||
if ($this->isBlocked($email) === true) {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
throw new PermissionException('Rate limit exceeded');
|
||||
}
|
||||
|
||||
// time-limiting
|
||||
$timeout = $session->get('kirby.challenge.timeout');
|
||||
if ($timeout !== null && time() > $timeout) {
|
||||
throw new PermissionException('Authentication challenge timeout');
|
||||
}
|
||||
$this->checkRateLimit($email);
|
||||
|
||||
if (
|
||||
isset(static::$challenges[$challenge]) === true &&
|
||||
class_exists(static::$challenges[$challenge]) === true &&
|
||||
is_subclass_of(static::$challenges[$challenge], 'Kirby\Cms\Auth\Challenge') === true
|
||||
is_subclass_of(static::$challenges[$challenge], Challenge::class) === true
|
||||
) {
|
||||
$class = static::$challenges[$challenge];
|
||||
if ($class::verify($user, $code) === true) {
|
||||
|
@ -839,29 +861,67 @@ class Auth
|
|||
$this->status = null;
|
||||
|
||||
return $user;
|
||||
} else {
|
||||
throw new PermissionException(['key' => 'access.code']);
|
||||
}
|
||||
|
||||
throw new PermissionException(['key' => 'access.code']);
|
||||
}
|
||||
|
||||
throw new LogicException('Invalid authentication challenge: ' . $challenge);
|
||||
} catch (Throwable $e) {
|
||||
if (empty($email) === false && $e->getMessage() !== 'Rate limit exceeded') {
|
||||
$details = $e instanceof \Kirby\Exception\Exception ? $e->getDetails() : [];
|
||||
|
||||
if (
|
||||
empty($email) === false &&
|
||||
($details['reason'] ?? null) !== 'rate-limited'
|
||||
) {
|
||||
$this->track($email);
|
||||
}
|
||||
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder and to
|
||||
// avoid leaking whether the user exists
|
||||
usleep(random_int(1000, 2000000));
|
||||
usleep(random_int(10000, 2000000));
|
||||
|
||||
// specifically copy over the marker for a destroyed challenge
|
||||
// even in production (used by the Panel to reset to the login form)
|
||||
$challengeDestroyed = $details['challengeDestroyed'] ?? false;
|
||||
|
||||
$fallback = new PermissionException([
|
||||
'details' => compact('challengeDestroyed'),
|
||||
'key' => 'access.code'
|
||||
]);
|
||||
|
||||
// keep throwing the original error in debug mode,
|
||||
// otherwise hide it to avoid leaking security-relevant information
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
throw $e;
|
||||
} else {
|
||||
throw new PermissionException(['key' => 'access.code']);
|
||||
}
|
||||
$this->fail($e, $fallback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception only in debug mode, otherwise falls back
|
||||
* to a public error without sensitive information
|
||||
*
|
||||
* @throws \Throwable Either the passed `$exception` or the `$fallback`
|
||||
* (no exception if debugging is disabled and no fallback was passed)
|
||||
*/
|
||||
protected function fail(Throwable $exception, Throwable $fallback = null): void
|
||||
{
|
||||
$debug = $this->kirby->option('auth.debug', 'log');
|
||||
|
||||
// throw the original exception only in debug mode
|
||||
if ($debug === true) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
// otherwise hide the real error and only print it to the error log
|
||||
// unless disabled by setting `auth.debug` to `false`
|
||||
if ($debug === 'log') {
|
||||
error_log($exception); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// only throw an error in production if requested by the calling method
|
||||
if ($fallback !== null) {
|
||||
throw $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -879,7 +939,7 @@ class Auth
|
|||
}
|
||||
|
||||
// try session in header or cookie
|
||||
if (is_a($session, 'Kirby\Session\Session') === false) {
|
||||
if ($session instanceof Session === false) {
|
||||
return $this->kirby->session(['detect' => true]);
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ abstract class Challenge
|
|||
* @return string|null The generated and sent code or `null` in case
|
||||
* there was no code to generate by this algorithm
|
||||
*/
|
||||
abstract public static function create(User $user, array $options): ?string;
|
||||
abstract public static function create(User $user, array $options): string|null;
|
||||
|
||||
/**
|
||||
* Verifies the provided code against the created one;
|
||||
|
|
|
@ -86,7 +86,7 @@ class Status
|
|||
* user to avoid leaking whether the pending user exists
|
||||
* @return string|null
|
||||
*/
|
||||
public function challenge(bool $automaticFallback = true): ?string
|
||||
public function challenge(bool $automaticFallback = true): string|null
|
||||
{
|
||||
// never return a challenge type if the status doesn't match
|
||||
if ($this->status() !== 'pending') {
|
||||
|
@ -95,9 +95,9 @@ class Status
|
|||
|
||||
if ($automaticFallback === false) {
|
||||
return $this->challenge;
|
||||
} else {
|
||||
return $this->challenge ?? $this->challengeFallback;
|
||||
}
|
||||
|
||||
return $this->challenge ?? $this->challengeFallback;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -105,7 +105,7 @@ class Status
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function email(): ?string
|
||||
public function email(): string|null
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ class Status
|
|||
* @param string|null $challenge
|
||||
* @return $this
|
||||
*/
|
||||
protected function setChallenge(?string $challenge = null)
|
||||
protected function setChallenge(string|null $challenge = null)
|
||||
{
|
||||
$this->challenge = $challenge;
|
||||
return $this;
|
||||
|
@ -169,7 +169,7 @@ class Status
|
|||
* @param string|null $challengeFallback
|
||||
* @return $this
|
||||
*/
|
||||
protected function setChallengeFallback(?string $challengeFallback = null)
|
||||
protected function setChallengeFallback(string|null $challengeFallback = null)
|
||||
{
|
||||
$this->challengeFallback = $challengeFallback;
|
||||
return $this;
|
||||
|
@ -181,7 +181,7 @@ class Status
|
|||
* @param string|null $email
|
||||
* @return $this
|
||||
*/
|
||||
protected function setEmail(?string $email = null)
|
||||
protected function setEmail(string|null $email = null)
|
||||
{
|
||||
$this->email = $email;
|
||||
return $this;
|
||||
|
|
|
@ -22,7 +22,7 @@ class Block extends Item
|
|||
{
|
||||
use HasMethods;
|
||||
|
||||
public const ITEMS_CLASS = '\Kirby\Cms\Blocks';
|
||||
public const ITEMS_CLASS = Blocks::class;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Content
|
||||
|
@ -157,7 +157,7 @@ class Block extends Item
|
|||
if (empty($type) === false && $class = (static::$models[$type] ?? null)) {
|
||||
$object = new $class($params);
|
||||
|
||||
if (is_a($object, 'Kirby\Cms\Block') === true) {
|
||||
if ($object instanceof self) {
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ class Block extends Item
|
|||
if ($class = (static::$models['Kirby\Cms\Block'] ?? null)) {
|
||||
$object = new $class($params);
|
||||
|
||||
if (is_a($object, 'Kirby\Cms\Block') === true) {
|
||||
if ($object instanceof self) {
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use Throwable;
|
|||
*/
|
||||
class Blocks extends Items
|
||||
{
|
||||
public const ITEM_CLASS = '\Kirby\Cms\Block';
|
||||
public const ITEM_CLASS = Block::class;
|
||||
|
||||
/**
|
||||
* Return HTML when the collection is
|
||||
|
@ -114,7 +114,7 @@ class Blocks extends Items
|
|||
if (empty($input) === false && is_array($input) === false) {
|
||||
try {
|
||||
$input = Json::decode((string)$input);
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
$parser = new Parsley((string)$input, new BlockSchema());
|
||||
$input = $parser->blocks();
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ class Blueprint
|
|||
throw new InvalidArgumentException('A blueprint model is required');
|
||||
}
|
||||
|
||||
if (is_a($props['model'], ModelWithContent::class) === false) {
|
||||
if ($props['model'] instanceof ModelWithContent === false) {
|
||||
throw new InvalidArgumentException('Invalid blueprint model');
|
||||
}
|
||||
|
||||
|
@ -208,7 +208,7 @@ class Blueprint
|
|||
$mixin = static::find($extend);
|
||||
$mixin = static::extend($mixin);
|
||||
$props = A::merge($mixin, $props, A::MERGE_REPLACE);
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
// keep the props unextended if the snippet wasn't found
|
||||
}
|
||||
}
|
||||
|
@ -231,7 +231,7 @@ class Blueprint
|
|||
{
|
||||
try {
|
||||
$props = static::load($name);
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
$props = $fallback !== null ? static::load($fallback) : null;
|
||||
}
|
||||
|
||||
|
@ -251,7 +251,7 @@ class Blueprint
|
|||
* @param string $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function field(string $name): ?array
|
||||
public function field(string $name): array|null
|
||||
{
|
||||
return $this->fields[$name] ?? null;
|
||||
}
|
||||
|
@ -297,7 +297,8 @@ class Blueprint
|
|||
// now ensure that we always return the data array
|
||||
if (is_string($file) === true && F::exists($file) === true) {
|
||||
return static::$loaded[$name] = Data::read($file);
|
||||
} elseif (is_array($file) === true) {
|
||||
}
|
||||
if (is_array($file) === true) {
|
||||
return static::$loaded[$name] = $file;
|
||||
}
|
||||
|
||||
|
@ -398,9 +399,9 @@ class Blueprint
|
|||
if (empty($columnProps['sections']) === true) {
|
||||
$columnProps['sections'] = [
|
||||
$tabName . '-info-' . $columnKey => [
|
||||
'headline' => 'Column (' . ($columnProps['width'] ?? '1/1') . ')',
|
||||
'type' => 'info',
|
||||
'text' => 'No sections yet'
|
||||
'label' => 'Column (' . ($columnProps['width'] ?? '1/1') . ')',
|
||||
'type' => 'info',
|
||||
'text' => 'No sections yet'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
@ -620,17 +621,17 @@ class Blueprint
|
|||
|
||||
if (empty($type) === true || is_string($type) === false) {
|
||||
$sections[$sectionName] = [
|
||||
'name' => $sectionName,
|
||||
'headline' => 'Invalid section type for section "' . $sectionName . '"',
|
||||
'type' => 'info',
|
||||
'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types))
|
||||
'name' => $sectionName,
|
||||
'label' => 'Invalid section type for section "' . $sectionName . '"',
|
||||
'type' => 'info',
|
||||
'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types))
|
||||
];
|
||||
} elseif (isset(Section::$types[$type]) === false) {
|
||||
$sections[$sectionName] = [
|
||||
'name' => $sectionName,
|
||||
'headline' => 'Invalid section type ("' . $type . '")',
|
||||
'type' => 'info',
|
||||
'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types))
|
||||
'name' => $sectionName,
|
||||
'label' => 'Invalid section type ("' . $type . '")',
|
||||
'type' => 'info',
|
||||
'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types))
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -730,7 +731,7 @@ class Blueprint
|
|||
$preset = static::$presets[$props['preset']];
|
||||
|
||||
if (is_string($preset) === true) {
|
||||
$preset = require $preset;
|
||||
$preset = F::load($preset, allowOutput: false);
|
||||
}
|
||||
|
||||
return $preset($props);
|
||||
|
@ -777,7 +778,7 @@ class Blueprint
|
|||
* @param string|null $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function tab(?string $name = null): ?array
|
||||
public function tab(string|null $name = null): array|null
|
||||
{
|
||||
if ($name === null) {
|
||||
return A::first($this->tabs);
|
||||
|
|
|
@ -6,6 +6,7 @@ use Closure;
|
|||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Collection as BaseCollection;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* The Collection class serves as foundation
|
||||
|
@ -87,9 +88,12 @@ class Collection extends BaseCollection
|
|||
*/
|
||||
public function add($object)
|
||||
{
|
||||
if (is_a($object, self::class) === true) {
|
||||
if ($object instanceof self) {
|
||||
$this->data = array_merge($this->data, $object->data);
|
||||
} elseif (is_object($object) === true && method_exists($object, 'id') === true) {
|
||||
} elseif (
|
||||
is_object($object) === true &&
|
||||
method_exists($object, 'id') === true
|
||||
) {
|
||||
$this->__set($object->id(), $object);
|
||||
} else {
|
||||
$this->append($object);
|
||||
|
@ -110,16 +114,37 @@ class Collection extends BaseCollection
|
|||
{
|
||||
if (count($args) === 1) {
|
||||
// try to determine the key from the provided item
|
||||
if (is_object($args[0]) === true && is_callable([$args[0], 'id']) === true) {
|
||||
if (
|
||||
is_object($args[0]) === true &&
|
||||
is_callable([$args[0], 'id']) === true
|
||||
) {
|
||||
return parent::append($args[0]->id(), $args[0]);
|
||||
} else {
|
||||
return parent::append($args[0]);
|
||||
}
|
||||
|
||||
return parent::append($args[0]);
|
||||
}
|
||||
|
||||
return parent::append(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single element by an attribute and its value
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function findBy(string $attribute, $value)
|
||||
{
|
||||
// $value: cast UUID object to string to allow uses
|
||||
// like `$pages->findBy('related', $page->uuid())`
|
||||
if ($value instanceof Uuid) {
|
||||
$value = $value->toString();
|
||||
}
|
||||
|
||||
return parent::findBy($attribute, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the items by a given field or callback. Returns a collection
|
||||
* with an item for each group and a collection for each group.
|
||||
|
@ -208,7 +233,9 @@ class Collection extends BaseCollection
|
|||
foreach ($keys as $key) {
|
||||
if (is_array($key) === true) {
|
||||
return $this->not(...$key);
|
||||
} elseif (is_a($key, 'Kirby\Toolkit\Collection') === true) {
|
||||
}
|
||||
|
||||
if ($key instanceof BaseCollection) {
|
||||
$collection = $collection->not(...$key->keys());
|
||||
} elseif (is_object($key) === true) {
|
||||
$key = $key->id();
|
||||
|
@ -256,11 +283,14 @@ class Collection extends BaseCollection
|
|||
{
|
||||
if (count($args) === 1) {
|
||||
// try to determine the key from the provided item
|
||||
if (is_object($args[0]) === true && is_callable([$args[0], 'id']) === true) {
|
||||
if (
|
||||
is_object($args[0]) === true &&
|
||||
is_callable([$args[0], 'id']) === true
|
||||
) {
|
||||
return parent::prepend($args[0]->id(), $args[0]);
|
||||
} else {
|
||||
return parent::prepend($args[0]);
|
||||
}
|
||||
|
||||
return parent::prepend($args[0]);
|
||||
}
|
||||
|
||||
return parent::prepend(...$args);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Toolkit\Controller;
|
||||
|
@ -62,9 +63,7 @@ class Collections
|
|||
public function get(string $name, array $data = [])
|
||||
{
|
||||
// if not yet loaded
|
||||
if (isset($this->collections[$name]) === false) {
|
||||
$this->collections[$name] = $this->load($name);
|
||||
}
|
||||
$this->collections[$name] ??= $this->load($name);
|
||||
|
||||
// if not yet cached
|
||||
if (
|
||||
|
@ -101,7 +100,7 @@ class Collections
|
|||
try {
|
||||
$this->load($name);
|
||||
return true;
|
||||
} catch (NotFoundException $e) {
|
||||
} catch (NotFoundException) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -122,9 +121,9 @@ class Collections
|
|||
$file = $kirby->root('collections') . '/' . $name . '.php';
|
||||
|
||||
if (is_file($file) === true) {
|
||||
$collection = F::load($file);
|
||||
$collection = F::load($file, allowOutput: false);
|
||||
|
||||
if (is_a($collection, 'Closure')) {
|
||||
if ($collection instanceof Closure) {
|
||||
return $collection;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@ class Content
|
|||
$oldField = $oldFields->get($name);
|
||||
|
||||
// field name and type matches with old template
|
||||
if ($oldField && $oldField->type() === $newField->type()) {
|
||||
if ($oldField?->type() === $newField->type()) {
|
||||
$data[$name] = $content->get($name)->value();
|
||||
} else {
|
||||
$data[$name] = $newField->default();
|
||||
|
@ -164,13 +164,11 @@ class Content
|
|||
|
||||
$key = strtolower($key);
|
||||
|
||||
if (isset($this->fields[$key])) {
|
||||
return $this->fields[$key];
|
||||
}
|
||||
|
||||
$value = $this->data()[$key] ?? null;
|
||||
|
||||
return $this->fields[$key] = new Field($this->parent, $key, $value);
|
||||
return $this->fields[$key] ??= new Field(
|
||||
$this->parent,
|
||||
$key,
|
||||
$this->data()[$key] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -195,6 +195,33 @@ class ContentLock
|
|||
return $this->kirby()->locks()->set($this->model, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state for the
|
||||
* form buttons in the frontend
|
||||
*/
|
||||
public function state(): ?string
|
||||
{
|
||||
return match (true) {
|
||||
$this->isUnlocked() => 'unlock',
|
||||
$this->isLocked() => 'lock',
|
||||
default => null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a usable lock array
|
||||
* for the frontend
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'state' => $this->state(),
|
||||
'data' => $this->get()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes current lock and adds lock user to unlock data
|
||||
*
|
||||
|
@ -223,10 +250,7 @@ class ContentLock
|
|||
*/
|
||||
protected function user(): User
|
||||
{
|
||||
if ($user = $this->kirby()->user()) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
throw new PermissionException('No user authenticated.');
|
||||
return $this->kirby()->user() ??
|
||||
throw new PermissionException('No user authenticated.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,23 +84,17 @@ class ContentTranslation
|
|||
*/
|
||||
public function content(): array
|
||||
{
|
||||
$parent = $this->parent();
|
||||
|
||||
if ($this->content === null) {
|
||||
$this->content = $parent->readContent($this->code());
|
||||
}
|
||||
|
||||
$content = $this->content;
|
||||
$parent = $this->parent();
|
||||
$content = $this->content ??= $parent->readContent($this->code());
|
||||
|
||||
// merge with the default content
|
||||
if ($this->isDefault() === false && $defaultLanguage = $parent->kirby()->defaultLanguage()) {
|
||||
$default = [];
|
||||
|
||||
if ($defaultTranslation = $parent->translation($defaultLanguage->code())) {
|
||||
$default = $defaultTranslation->content();
|
||||
if (
|
||||
$this->isDefault() === false &&
|
||||
$defaultLanguage = $parent->kirby()->defaultLanguage()
|
||||
) {
|
||||
if ($default = $parent->translation($defaultLanguage->code())?->content()) {
|
||||
$content = array_merge($default, $content);
|
||||
}
|
||||
|
||||
$content = array_merge($default, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
|
@ -118,12 +112,11 @@ class ContentTranslation
|
|||
|
||||
/**
|
||||
* Checks if the translation file exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->contentFile()) === true;
|
||||
return empty($this->content) === false ||
|
||||
file_exists($this->contentFile()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -211,7 +204,7 @@ class ContentTranslation
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function slug(): ?string
|
||||
public function slug(): string|null
|
||||
{
|
||||
return $this->slug ??= ($this->content()['slug'] ?? null);
|
||||
}
|
||||
|
|
|
@ -21,24 +21,10 @@ namespace Kirby\Cms;
|
|||
*/
|
||||
class Core
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $cache = [];
|
||||
protected array $cache = [];
|
||||
protected App $kirby;
|
||||
protected string $root;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\App
|
||||
*/
|
||||
protected $kirby;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
*/
|
||||
public function __construct(App $kirby)
|
||||
{
|
||||
$this->kirby = $kirby;
|
||||
|
@ -50,11 +36,8 @@ class Core
|
|||
*
|
||||
* This is a shortcut for `$kirby->core()->load()->area()`
|
||||
* to give faster access to original area code in plugins.
|
||||
*
|
||||
* @param string $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function area(string $name): ?array
|
||||
public function area(string $name): array|null
|
||||
{
|
||||
return $this->load()->area($name);
|
||||
}
|
||||
|
@ -63,8 +46,6 @@ class Core
|
|||
* Returns a list of all paths to area definition files
|
||||
*
|
||||
* They are located in `/kirby/config/areas`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function areas(): array
|
||||
{
|
||||
|
@ -82,8 +63,6 @@ class Core
|
|||
|
||||
/**
|
||||
* Returns a list of all default auth challenge classes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function authChallenges(): array
|
||||
{
|
||||
|
@ -96,8 +75,6 @@ class Core
|
|||
* Returns a list of all paths to blueprint presets
|
||||
*
|
||||
* They are located in `/kirby/config/presets`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function blueprintPresets(): array
|
||||
{
|
||||
|
@ -113,8 +90,6 @@ class Core
|
|||
*
|
||||
* They are located in `/kirby/config/blueprints`.
|
||||
* Block blueprints are located in `/kirby/config/blocks`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function blueprints(): array
|
||||
{
|
||||
|
@ -143,10 +118,19 @@ class Core
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all core caches
|
||||
*/
|
||||
public function caches(): array
|
||||
{
|
||||
return [
|
||||
'updates' => true,
|
||||
'uuid' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all cache driver classes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function cacheTypes(): array
|
||||
{
|
||||
|
@ -163,8 +147,6 @@ class Core
|
|||
*
|
||||
* The component functions can be found in
|
||||
* `/kirby/config/components.php`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function components(): array
|
||||
{
|
||||
|
@ -173,8 +155,6 @@ class Core
|
|||
|
||||
/**
|
||||
* Returns a map of all field method aliases
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fieldMethodAliases(): array
|
||||
{
|
||||
|
@ -199,8 +179,6 @@ class Core
|
|||
* Returns an array of all field method functions
|
||||
*
|
||||
* Field methods are stored in `/kirby/config/methods.php`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fieldMethods(): array
|
||||
{
|
||||
|
@ -211,8 +189,6 @@ class Core
|
|||
* Returns an array of paths for field mixins
|
||||
*
|
||||
* They are located in `/kirby/config/fields/mixins`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fieldMixins(): array
|
||||
{
|
||||
|
@ -236,8 +212,6 @@ class Core
|
|||
*
|
||||
* The more complex field classes can be found in
|
||||
* `/kirby/src/Form/Fields`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fields(): array
|
||||
{
|
||||
|
@ -256,6 +230,7 @@ class Core
|
|||
'list' => $this->root . '/fields/list.php',
|
||||
'multiselect' => $this->root . '/fields/multiselect.php',
|
||||
'number' => $this->root . '/fields/number.php',
|
||||
'object' => $this->root . '/fields/object.php',
|
||||
'pages' => $this->root . '/fields/pages.php',
|
||||
'radio' => $this->root . '/fields/radio.php',
|
||||
'range' => $this->root . '/fields/range.php',
|
||||
|
@ -277,8 +252,6 @@ class Core
|
|||
|
||||
/**
|
||||
* Returns a map of all kirbytag aliases
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function kirbyTagAliases(): array
|
||||
{
|
||||
|
@ -292,8 +265,6 @@ class Core
|
|||
* Returns an array of all kirbytag definitions
|
||||
*
|
||||
* They are located in `/kirby/config/tags.php`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function kirbyTags(): array
|
||||
{
|
||||
|
@ -306,10 +277,8 @@ class Core
|
|||
* The loader is set to not include plugins.
|
||||
* This way, you can access original Kirby core code
|
||||
* through this load method.
|
||||
*
|
||||
* @return \Kirby\Cms\Loader
|
||||
*/
|
||||
public function load()
|
||||
public function load(): Loader
|
||||
{
|
||||
return new Loader($this->kirby, false);
|
||||
}
|
||||
|
@ -318,8 +287,6 @@ class Core
|
|||
* Returns all absolute paths to important directories
|
||||
*
|
||||
* Roots are resolved and baked in `\Kirby\Cms\App::bakeRoots()`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function roots(): array
|
||||
{
|
||||
|
@ -339,6 +306,7 @@ class Core
|
|||
'blueprints' => fn (array $roots) => $roots['site'] . '/blueprints',
|
||||
'cache' => fn (array $roots) => $roots['site'] . '/cache',
|
||||
'collections' => fn (array $roots) => $roots['site'] . '/collections',
|
||||
'commands' => fn (array $roots) => $roots['site'] . '/commands',
|
||||
'config' => fn (array $roots) => $roots['site'] . '/config',
|
||||
'controllers' => fn (array $roots) => $roots['site'] . '/controllers',
|
||||
'languages' => fn (array $roots) => $roots['site'] . '/languages',
|
||||
|
@ -359,8 +327,6 @@ class Core
|
|||
* Routes are split into `before` and `after` routes.
|
||||
*
|
||||
* Plugin routes will be injected inbetween.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function routes(): array
|
||||
{
|
||||
|
@ -371,8 +337,6 @@ class Core
|
|||
* Returns a list of all paths to core block snippets
|
||||
*
|
||||
* They are located in `/kirby/config/blocks`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function snippets(): array
|
||||
{
|
||||
|
@ -395,8 +359,6 @@ class Core
|
|||
* Returns a list of paths to section mixins
|
||||
*
|
||||
* They are located in `/kirby/config/sections/mixins`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sectionMixins(): array
|
||||
{
|
||||
|
@ -419,8 +381,6 @@ class Core
|
|||
* Returns a list of all section definitions
|
||||
*
|
||||
* They are located in `/kirby/config/sections`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sections(): array
|
||||
{
|
||||
|
@ -437,8 +397,6 @@ class Core
|
|||
* Returns a list of paths to all system templates
|
||||
*
|
||||
* They are located in `/kirby/config/templates`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function templates(): array
|
||||
{
|
||||
|
@ -452,8 +410,6 @@ class Core
|
|||
* Returns an array with all system URLs
|
||||
*
|
||||
* URLs are resolved and baked in `\Kirby\Cms\App::bakeUrls()`
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function urls(): array
|
||||
{
|
||||
|
@ -465,9 +421,9 @@ class Core
|
|||
|
||||
if (empty($path) === true) {
|
||||
return $urls['index'];
|
||||
} else {
|
||||
return $urls['base'] . '/' . $path;
|
||||
}
|
||||
|
||||
return $urls['base'] . '/' . $path;
|
||||
},
|
||||
'assets' => fn (array $urls) => $urls['base'] . '/assets',
|
||||
'api' => fn (array $urls) => $urls['base'] . '/' . $this->kirby->option('api.slug', 'api'),
|
||||
|
|
|
@ -49,14 +49,10 @@ class Email
|
|||
$this->props = array_merge($preset, $props);
|
||||
|
||||
// add transport settings
|
||||
if (isset($this->props['transport']) === false) {
|
||||
$this->props['transport'] = $this->options['transport'] ?? [];
|
||||
}
|
||||
$this->props['transport'] ??= $this->options['transport'] ?? [];
|
||||
|
||||
// add predefined beforeSend option
|
||||
if (isset($this->props['beforeSend']) === false) {
|
||||
$this->props['beforeSend'] = $this->options['beforeSend'] ?? null;
|
||||
}
|
||||
$this->props['beforeSend'] ??= $this->options['beforeSend'] ?? null;
|
||||
|
||||
// transform model objects to values
|
||||
$this->transformUserSingle('from', 'fromName');
|
||||
|
@ -193,7 +189,7 @@ class Email
|
|||
} else {
|
||||
$result[] = $item;
|
||||
}
|
||||
} elseif (is_a($item, $class) === true) {
|
||||
} elseif ($item instanceof $class) {
|
||||
// value is a model object, get value through content method(s)
|
||||
if ($contentKey !== null) {
|
||||
$result[(string)$item->$contentKey()] = (string)$item->$contentValue();
|
||||
|
@ -235,9 +231,7 @@ class Email
|
|||
$this->props[$addressProp] = $address;
|
||||
|
||||
// only use the name from the user if no custom name was set
|
||||
if (isset($this->props[$nameProp]) === false || $this->props[$nameProp] === null) {
|
||||
$this->props[$nameProp] = $name;
|
||||
}
|
||||
$this->props[$nameProp] ??= $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -120,7 +120,7 @@ class Event
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function action(): ?string
|
||||
public function action(): string|null
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
@ -133,11 +133,7 @@ class Event
|
|||
*/
|
||||
public function argument(string $name)
|
||||
{
|
||||
if (isset($this->arguments[$name]) === true) {
|
||||
return $this->arguments[$name];
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->arguments[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -158,7 +154,7 @@ class Event
|
|||
* @param \Closure $hook
|
||||
* @return mixed
|
||||
*/
|
||||
public function call(?object $bind, Closure $hook)
|
||||
public function call(object|null $bind, Closure $hook)
|
||||
{
|
||||
// collect the list of possible hook arguments
|
||||
$data = $this->arguments();
|
||||
|
@ -204,7 +200,9 @@ class Event
|
|||
'*:' . $this->state,
|
||||
'*'
|
||||
];
|
||||
} elseif ($this->state !== null) {
|
||||
}
|
||||
|
||||
if ($this->state !== null) {
|
||||
// event without action: $type:$state
|
||||
|
||||
return [
|
||||
|
@ -212,7 +210,9 @@ class Event
|
|||
'*:' . $this->state,
|
||||
'*'
|
||||
];
|
||||
} elseif ($this->action !== null) {
|
||||
}
|
||||
|
||||
if ($this->action !== null) {
|
||||
// event without state: $type.$action
|
||||
|
||||
return [
|
||||
|
@ -220,11 +220,10 @@ class Event
|
|||
'*.' . $this->action,
|
||||
'*'
|
||||
];
|
||||
} else {
|
||||
// event with a simple name
|
||||
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
// event with a simple name
|
||||
return ['*'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -232,7 +231,7 @@ class Event
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function state(): ?string
|
||||
public function state(): string|null
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ class Field
|
|||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct(?object $parent, string $key, $value)
|
||||
public function __construct(object|null $parent, string $key, $value)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->value = $value;
|
||||
|
@ -187,7 +187,7 @@ class Field
|
|||
return $this;
|
||||
}
|
||||
|
||||
if (is_a($fallback, 'Kirby\Cms\Field') === true) {
|
||||
if ($fallback instanceof self) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ use Kirby\Toolkit\Str;
|
|||
*/
|
||||
class Fieldset extends Item
|
||||
{
|
||||
public const ITEMS_CLASS = '\Kirby\Cms\Fieldsets';
|
||||
public const ITEMS_CLASS = Fieldsets::class;
|
||||
|
||||
protected $disabled;
|
||||
protected $editable;
|
||||
|
@ -92,7 +92,7 @@ class Fieldset extends Item
|
|||
* @param array|string $name
|
||||
* @return string|null
|
||||
*/
|
||||
protected function createName($name): ?string
|
||||
protected function createName($name): string|null
|
||||
{
|
||||
return I18n::translate($name, $name);
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ class Fieldset extends Item
|
|||
* @param array|string $label
|
||||
* @return string|null
|
||||
*/
|
||||
protected function createLabel($label = null): ?string
|
||||
protected function createLabel($label = null): string|null
|
||||
{
|
||||
return I18n::translate($label, $label);
|
||||
}
|
||||
|
@ -195,7 +195,7 @@ class Fieldset extends Item
|
|||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function icon(): ?string
|
||||
public function icon(): string|null
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ class Fieldset extends Item
|
|||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function label(): ?string
|
||||
public function label(): string|null
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ use Kirby\Toolkit\Str;
|
|||
*/
|
||||
class Fieldsets extends Items
|
||||
{
|
||||
public const ITEM_CLASS = '\Kirby\Cms\Fieldset';
|
||||
public const ITEM_CLASS = Fieldset::class;
|
||||
|
||||
protected static function createFieldsets($params)
|
||||
{
|
||||
|
@ -93,7 +93,7 @@ class Fieldsets extends Items
|
|||
return $this->options['groups'] ?? [];
|
||||
}
|
||||
|
||||
public function toArray(?Closure $map = null): array
|
||||
public function toArray(Closure|null $map = null): array
|
||||
{
|
||||
return A::map(
|
||||
$this->data,
|
||||
|
|
|
@ -71,10 +71,8 @@ class File extends ModelWithContent
|
|||
|
||||
/**
|
||||
* The absolute path to the file
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $root;
|
||||
protected string|null $root = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
|
@ -83,10 +81,8 @@ class File extends ModelWithContent
|
|||
|
||||
/**
|
||||
* The public file Url
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
protected string|null $url = null;
|
||||
|
||||
/**
|
||||
* Magic caller for file methods
|
||||
|
@ -164,7 +160,7 @@ class File extends ModelWithContent
|
|||
*/
|
||||
public function blueprint()
|
||||
{
|
||||
if (is_a($this->blueprint, 'Kirby\Cms\FileBlueprint') === true) {
|
||||
if ($this->blueprint instanceof FileBlueprint) {
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
|
@ -267,9 +263,10 @@ class File extends ModelWithContent
|
|||
return $this->id;
|
||||
}
|
||||
|
||||
if (is_a($this->parent(), 'Kirby\Cms\Page') === true) {
|
||||
return $this->id = $this->parent()->id() . '/' . $this->filename();
|
||||
} elseif (is_a($this->parent(), 'Kirby\Cms\User') === true) {
|
||||
if (
|
||||
$this->parent() instanceof Page ||
|
||||
$this->parent() instanceof User
|
||||
) {
|
||||
return $this->id = $this->parent()->id() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
|
@ -298,11 +295,7 @@ class File extends ModelWithContent
|
|||
|
||||
$template = $this->template();
|
||||
|
||||
if (isset($readable[$template]) === true) {
|
||||
return $readable[$template];
|
||||
}
|
||||
|
||||
return $readable[$template] = $this->permissions()->can('read');
|
||||
return $readable[$template] ??= $this->permissions()->can('read');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -398,7 +391,11 @@ class File extends ModelWithContent
|
|||
*/
|
||||
public function page()
|
||||
{
|
||||
return is_a($this->parent(), 'Kirby\Cms\Page') === true ? $this->parent() : null;
|
||||
if ($this->parent() instanceof Page) {
|
||||
return $this->parent();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -439,13 +436,22 @@ class File extends ModelWithContent
|
|||
*/
|
||||
public function parents()
|
||||
{
|
||||
if (is_a($this->parent(), 'Kirby\Cms\Page') === true) {
|
||||
if ($this->parent() instanceof Page) {
|
||||
return $this->parent()->parents()->prepend($this->parent()->id(), $this->parent());
|
||||
}
|
||||
|
||||
return new Pages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the permanent URL to the file using its UUID
|
||||
* @since 3.8.0
|
||||
*/
|
||||
public function permalink(): string|null
|
||||
{
|
||||
return $this->uuid()?->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the permissions object for this file
|
||||
*
|
||||
|
@ -461,7 +467,7 @@ class File extends ModelWithContent
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function root(): ?string
|
||||
public function root(): string|null
|
||||
{
|
||||
return $this->root ??= $this->parent()->root() . '/' . $this->filename();
|
||||
}
|
||||
|
@ -570,7 +576,11 @@ class File extends ModelWithContent
|
|||
*/
|
||||
public function site()
|
||||
{
|
||||
return is_a($this->parent(), 'Kirby\Cms\Site') === true ? $this->parent() : $this->kirby()->site();
|
||||
if ($this->parent() instanceof Site) {
|
||||
return $this->parent();
|
||||
}
|
||||
|
||||
return $this->kirby()->site();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -578,7 +588,7 @@ class File extends ModelWithContent
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function template(): ?string
|
||||
public function template(): string|null
|
||||
{
|
||||
return $this->template ??= $this->content()->get('template')->value();
|
||||
}
|
||||
|
@ -616,98 +626,6 @@ class File extends ModelWithContent
|
|||
return $this->url ??= ($this->kirby()->component('file::url'))($this->kirby(), $this);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deprecated!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides a kirbytag or markdown
|
||||
* tag for the file, which will be
|
||||
* used in the panel, when the file
|
||||
* gets dragged onto a textarea
|
||||
*
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $type (null|auto|kirbytext|markdown)
|
||||
* @param bool $absolute
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function dragText(string $type = null, bool $absolute = false): string
|
||||
{
|
||||
Helpers::deprecated('Cms\File::dragText() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->dragText() instead.');
|
||||
return $this->panel()->dragText($type, $absolute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all actions
|
||||
* that can be performed in the Panel
|
||||
*
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @since 3.3.0 This also checks for the lock status
|
||||
* @since 3.5.1 This also checks for matching accept settings
|
||||
*
|
||||
* @param array $unlock An array of options that will be force-unlocked
|
||||
* @return array
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelOptions(array $unlock = []): array
|
||||
{
|
||||
Helpers::deprecated('Cms\File::panelOptions() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->options() instead.');
|
||||
return $this->panel()->options($unlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
Helpers::deprecated('Cms\File::panelPath() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->path() instead.');
|
||||
return $this->panel()->path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the response data for file pickers
|
||||
* and file fields
|
||||
*
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @param array|null $params
|
||||
* @return array
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelPickerData(array $params = []): array
|
||||
{
|
||||
Helpers::deprecated('Cms\File::panelPickerData() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->pickerData() instead.');
|
||||
return $this->panel()->pickerData($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelUrl(bool $relative = false): string
|
||||
{
|
||||
Helpers::deprecated('Cms\File::panelUrl() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->url() instead.');
|
||||
return $this->panel()->url($relative);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified File URL that uses the parent
|
||||
* Page URL and the filename as a more stable
|
||||
|
|
|
@ -7,6 +7,8 @@ use Kirby\Exception\InvalidArgumentException;
|
|||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Form\Form;
|
||||
use Kirby\Uuid\Uuid;
|
||||
use Kirby\Uuid\Uuids;
|
||||
|
||||
/**
|
||||
* FileActions
|
||||
|
@ -44,6 +46,9 @@ trait FileActions
|
|||
'filename' => $name . '.' . $oldFile->extension(),
|
||||
]);
|
||||
|
||||
// remove all public versions, lock and clear UUID cache
|
||||
$oldFile->unpublish();
|
||||
|
||||
if ($oldFile->exists() === false) {
|
||||
return $newFile;
|
||||
}
|
||||
|
@ -52,14 +57,6 @@ trait FileActions
|
|||
throw new LogicException('The new file exists and cannot be overwritten');
|
||||
}
|
||||
|
||||
// remove the lock of the old file
|
||||
if ($lock = $oldFile->lock()) {
|
||||
$lock->remove();
|
||||
}
|
||||
|
||||
// remove all public versions
|
||||
$oldFile->unpublish();
|
||||
|
||||
// rename the main file
|
||||
F::move($oldFile->root(), $newFile->root());
|
||||
|
||||
|
@ -75,6 +72,7 @@ trait FileActions
|
|||
F::move($oldFile->contentFile(), $newFile->contentFile());
|
||||
}
|
||||
|
||||
// update collections
|
||||
$newFile->parent()->files()->remove($oldFile->id());
|
||||
$newFile->parent()->files()->set($newFile->id(), $newFile);
|
||||
|
||||
|
@ -122,13 +120,12 @@ trait FileActions
|
|||
|
||||
$result = $callback(...$argumentValues);
|
||||
|
||||
if ($action === 'create') {
|
||||
$argumentsAfter = ['file' => $result];
|
||||
} elseif ($action === 'delete') {
|
||||
$argumentsAfter = ['status' => $result, 'file' => $old];
|
||||
} else {
|
||||
$argumentsAfter = ['newFile' => $result, 'oldFile' => $old];
|
||||
}
|
||||
$argumentsAfter = match ($action) {
|
||||
'create' => ['file' => $result],
|
||||
'delete' => ['status' => $result, 'file' => $old],
|
||||
default => ['newFile' => $result, 'oldFile' => $old]
|
||||
};
|
||||
|
||||
$kirby->trigger('file.' . $action . ':after', $argumentsAfter);
|
||||
|
||||
$kirby->cache('pages')->flush();
|
||||
|
@ -155,7 +152,14 @@ trait FileActions
|
|||
F::copy($contentFile, $page->root() . '/' . basename($contentFile));
|
||||
}
|
||||
|
||||
return $page->clone()->file($this->filename());
|
||||
$copy = $page->clone()->file($this->filename());
|
||||
|
||||
// overwrite with new UUID (remove old, add new)
|
||||
if (Uuids::enabled() === true) {
|
||||
$copy = $copy->save(['uuid' => Uuid::generate()]);
|
||||
}
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -184,17 +188,24 @@ trait FileActions
|
|||
$file = static::factory($props);
|
||||
$upload = $file->asset($props['source']);
|
||||
|
||||
// gather content
|
||||
$content = $props['content'] ?? [];
|
||||
|
||||
// make sure that a UUID gets generated and
|
||||
// added to content right away
|
||||
if (Uuids::enabled() === true) {
|
||||
$content['uuid'] ??= Uuid::generate();
|
||||
}
|
||||
|
||||
// create a form for the file
|
||||
$form = Form::for($file, [
|
||||
'values' => $props['content'] ?? []
|
||||
]);
|
||||
$form = Form::for($file, ['values' => $content]);
|
||||
|
||||
// inject the content
|
||||
$file = $file->clone(['content' => $form->strings(true)]);
|
||||
|
||||
// run the hook
|
||||
return $file->commit('create', compact('file', 'upload'), function ($file, $upload) {
|
||||
// delete all public versions
|
||||
// remove all public versions, lock and clear UUID cache
|
||||
$file->unpublish();
|
||||
|
||||
// overwrite the original
|
||||
|
@ -229,14 +240,9 @@ trait FileActions
|
|||
public function delete(): bool
|
||||
{
|
||||
return $this->commit('delete', ['file' => $this], function ($file) {
|
||||
// remove all versions in the media folder
|
||||
// remove all public versions, lock and clear UUID cache
|
||||
$file->unpublish();
|
||||
|
||||
// remove the lock of the old file
|
||||
if ($lock = $file->lock()) {
|
||||
$lock->remove();
|
||||
}
|
||||
|
||||
if ($file->kirby()->multilang() === true) {
|
||||
foreach ($file->translations() as $translation) {
|
||||
F::remove($file->contentFile($translation->code()));
|
||||
|
@ -288,7 +294,7 @@ trait FileActions
|
|||
|
||||
return $this->commit('replace', $arguments, function ($file, $upload) {
|
||||
// delete all public versions
|
||||
$file->unpublish();
|
||||
$file->unpublish(true);
|
||||
|
||||
// overwrite the original
|
||||
if (F::copy($upload->root(), $file->root(), true) !== true) {
|
||||
|
@ -300,14 +306,43 @@ trait FileActions
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the content on disk
|
||||
*
|
||||
* @internal
|
||||
* @param array|null $data
|
||||
* @param string|null $languageCode
|
||||
* @param bool $overwrite
|
||||
* @return static
|
||||
*/
|
||||
public function save(array $data = null, string $languageCode = null, bool $overwrite = false)
|
||||
{
|
||||
$file = parent::save($data, $languageCode, $overwrite);
|
||||
|
||||
// update model in siblings collection
|
||||
$file->parent()->files()->set($file->id(), $file);
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all public versions of this file
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function unpublish()
|
||||
public function unpublish(bool $onlyMedia = false)
|
||||
{
|
||||
// unpublish media files
|
||||
Media::unpublish($this->parent()->mediaRoot(), $this);
|
||||
|
||||
if ($onlyMedia !== true) {
|
||||
// remove the lock
|
||||
$this->lock()?->remove();
|
||||
|
||||
// clear UUID cache
|
||||
$this->uuid()?->clear();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Filesystem\Mime;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
|
@ -81,7 +82,10 @@ class FileBlueprint extends Blueprint
|
|||
|
||||
if (is_array($accept['extension']) === true) {
|
||||
// determine the main MIME type for each extension
|
||||
$restrictions[] = array_map(['Kirby\Filesystem\Mime', 'fromExtension'], $accept['extension']);
|
||||
$restrictions[] = array_map(
|
||||
[Mime::class, 'fromExtension'],
|
||||
$accept['extension']
|
||||
);
|
||||
}
|
||||
|
||||
if (is_array($accept['type']) === true) {
|
||||
|
@ -89,7 +93,10 @@ class FileBlueprint extends Blueprint
|
|||
$mimes = [];
|
||||
foreach ($accept['type'] as $type) {
|
||||
if ($extensions = F::typeToExtensions($type)) {
|
||||
$mimes[] = array_map(['Kirby\Filesystem\Mime', 'fromExtension'], $extensions);
|
||||
$mimes[] = array_map(
|
||||
[Mime::class, 'fromExtension'],
|
||||
$extensions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,19 +126,15 @@ class FileBlueprint extends Blueprint
|
|||
*/
|
||||
protected function normalizeAccept($accept = null): array
|
||||
{
|
||||
if (is_string($accept) === true) {
|
||||
$accept = [
|
||||
'mime' => $accept
|
||||
];
|
||||
} elseif ($accept === true) {
|
||||
$accept = match (true) {
|
||||
is_string($accept) => ['mime' => $accept],
|
||||
// explicitly no restrictions at all
|
||||
$accept = [
|
||||
'mime' => null
|
||||
];
|
||||
} elseif (empty($accept) === true) {
|
||||
$accept === true => ['mime' => null],
|
||||
// no custom restrictions
|
||||
$accept = [];
|
||||
}
|
||||
empty($accept) === true => [],
|
||||
// custom restrictions
|
||||
default => $accept
|
||||
};
|
||||
|
||||
$accept = array_change_key_case($accept);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Filesystem\Asset;
|
||||
|
||||
/**
|
||||
* Trait for image resizing, blurring etc.
|
||||
|
@ -53,7 +54,7 @@ trait FileModifications
|
|||
$quality = $options;
|
||||
} elseif (is_string($options)) {
|
||||
$crop = $options;
|
||||
} elseif (is_a($options, 'Kirby\Cms\Field') === true) {
|
||||
} elseif ($options instanceof Field) {
|
||||
$crop = $options->value();
|
||||
} elseif (is_array($options)) {
|
||||
$quality = $options['quality'] ?? $quality;
|
||||
|
@ -127,7 +128,7 @@ trait FileModifications
|
|||
* @param array|string|null $sizes
|
||||
* @return string|null
|
||||
*/
|
||||
public function srcset($sizes = null): ?string
|
||||
public function srcset($sizes = null): string|null
|
||||
{
|
||||
if (empty($sizes) === true) {
|
||||
$sizes = $this->kirby()->option('thumbs.srcsets.default', []);
|
||||
|
@ -202,9 +203,9 @@ trait FileModifications
|
|||
$result = $component($this->kirby(), $this, $options);
|
||||
|
||||
if (
|
||||
is_a($result, 'Kirby\Cms\FileVersion') === false &&
|
||||
is_a($result, 'Kirby\Cms\File') === false &&
|
||||
is_a($result, 'Kirby\Filesystem\Asset') === false
|
||||
$result instanceof FileVersion === false &&
|
||||
$result instanceof File === false &&
|
||||
$result instanceof Asset === false
|
||||
) {
|
||||
throw new InvalidArgumentException('The file::version component must return a File, FileVersion or Asset object');
|
||||
}
|
||||
|
|
|
@ -41,13 +41,14 @@ class FilePicker extends Picker
|
|||
$model = $this->options['model'];
|
||||
|
||||
// find the right default query
|
||||
if (empty($this->options['query']) === false) {
|
||||
$query = $this->options['query'];
|
||||
} elseif (is_a($model, 'Kirby\Cms\File') === true) {
|
||||
$query = 'file.siblings';
|
||||
} else {
|
||||
$query = $model::CLASS_ALIAS . '.files';
|
||||
}
|
||||
$query = match (true) {
|
||||
empty($this->options['query']) === false
|
||||
=> $this->options['query'],
|
||||
$model instanceof File
|
||||
=> 'file.siblings',
|
||||
default
|
||||
=> $model::CLASS_ALIAS . '.files'
|
||||
};
|
||||
|
||||
// fetch all files for the picker
|
||||
$files = $model->query($query);
|
||||
|
@ -55,15 +56,14 @@ class FilePicker extends Picker
|
|||
// help mitigate some typical query usage issues
|
||||
// by converting site and page objects to proper
|
||||
// pages by returning their children
|
||||
if (is_a($files, 'Kirby\Cms\Site') === true) {
|
||||
$files = $files->files();
|
||||
} elseif (is_a($files, 'Kirby\Cms\Page') === true) {
|
||||
$files = $files->files();
|
||||
} elseif (is_a($files, 'Kirby\Cms\User') === true) {
|
||||
$files = $files->files();
|
||||
} elseif (is_a($files, 'Kirby\Cms\Files') === false) {
|
||||
throw new InvalidArgumentException('Your query must return a set of files');
|
||||
}
|
||||
$files = match (true) {
|
||||
$files instanceof Site,
|
||||
$files instanceof Page,
|
||||
$files instanceof User => $files->files(),
|
||||
$files instanceof Files => $files,
|
||||
|
||||
default => throw new InvalidArgumentException('Your query must return a set of files')
|
||||
};
|
||||
|
||||
// search
|
||||
$files = $this->search($files);
|
||||
|
|
|
@ -45,7 +45,7 @@ class FileVersion
|
|||
}
|
||||
|
||||
// content fields
|
||||
if (is_a($this->original(), 'Kirby\Cms\File') === true) {
|
||||
if ($this->original() instanceof File) {
|
||||
return $this->original()->content()->get($method, $arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Kirby\Cms;
|
|||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Uuid\HasUuids;
|
||||
|
||||
/**
|
||||
* The `$files` object extends the general
|
||||
|
@ -21,6 +22,8 @@ use Kirby\Filesystem\F;
|
|||
*/
|
||||
class Files extends Collection
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
/**
|
||||
* All registered files methods
|
||||
*
|
||||
|
@ -40,15 +43,18 @@ class Files extends Collection
|
|||
public function add($object)
|
||||
{
|
||||
// add a files collection
|
||||
if (is_a($object, self::class) === true) {
|
||||
if ($object instanceof self) {
|
||||
$this->data = array_merge($this->data, $object->data);
|
||||
|
||||
// add a file by id
|
||||
} elseif (is_string($object) === true && $file = App::instance()->file($object)) {
|
||||
} elseif (
|
||||
is_string($object) === true &&
|
||||
$file = App::instance()->file($object)
|
||||
) {
|
||||
$this->__set($file->id(), $file);
|
||||
|
||||
// add a file object
|
||||
} elseif (is_a($object, 'Kirby\Cms\File') === true) {
|
||||
} elseif ($object instanceof File) {
|
||||
$this->__set($object->id(), $object);
|
||||
|
||||
// give a useful error message on invalid input;
|
||||
|
@ -105,22 +111,6 @@ class Files extends Collection
|
|||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a file by id/filename
|
||||
* @deprecated 3.7.0 Use `$files->find()` instead
|
||||
* @todo 3.8.0 Remove method
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $id
|
||||
* @return \Kirby\Cms\File|null
|
||||
*/
|
||||
public function findById(string $id)
|
||||
{
|
||||
Helpers::deprecated('Cms\Files::findById() has been deprecated and will be removed in Kirby 3.8.0. Use $files->find() instead.');
|
||||
|
||||
return $this->findByKey($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a file by its filename
|
||||
* @internal Use `$files->find()` instead
|
||||
|
@ -130,7 +120,11 @@ class Files extends Collection
|
|||
*/
|
||||
public function findByKey(string $key)
|
||||
{
|
||||
return $this->get(ltrim($this->parent->id() . '/' . $key, '/'));
|
||||
if ($file = $this->findByUuid($key, 'file')) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return $this->get(ltrim($this->parent?->id() . '/' . $key, '/'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -33,7 +33,7 @@ class Find
|
|||
$filename = urldecode($filename);
|
||||
$file = static::parent($path)->file($filename);
|
||||
|
||||
if ($file && $file->isReadable() === true) {
|
||||
if ($file?->isReadable() === true) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
|
@ -78,7 +78,7 @@ class Find
|
|||
$id = str_replace(['+', ' '], '/', $id);
|
||||
$page = App::instance()->page($id);
|
||||
|
||||
if ($page && $page->isReadable() === true) {
|
||||
if ($page?->isReadable() === true) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
|
@ -117,31 +117,16 @@ class Find
|
|||
|
||||
$kirby = App::instance();
|
||||
|
||||
switch ($modelName) {
|
||||
case 'site':
|
||||
$model = $kirby->site();
|
||||
break;
|
||||
case 'account':
|
||||
$model = static::user();
|
||||
break;
|
||||
case 'page':
|
||||
$model = static::page(basename($path));
|
||||
break;
|
||||
case 'file':
|
||||
$model = static::file(...explode('/files/', $path));
|
||||
break;
|
||||
case 'user':
|
||||
$model = $kirby->user(basename($path));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid model type: ' . $modelType);
|
||||
}
|
||||
$model = match ($modelName) {
|
||||
'site' => $kirby->site(),
|
||||
'account' => static::user(),
|
||||
'page' => static::page(basename($path)),
|
||||
'file' => static::file(...explode('/files/', $path)),
|
||||
'user' => $kirby->user(basename($path)),
|
||||
default => throw new InvalidArgumentException('Invalid model type: ' . $modelType)
|
||||
};
|
||||
|
||||
if ($model) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
return $model ?? throw new NotFoundException([
|
||||
'key' => $modelName . '.undefined'
|
||||
]);
|
||||
}
|
||||
|
@ -167,21 +152,18 @@ class Find
|
|||
|
||||
// get the authenticated user
|
||||
if ($id === null) {
|
||||
if ($user = $kirby->user(null, $kirby->option('api.allowImpersonation', false))) {
|
||||
return $user;
|
||||
}
|
||||
$user = $kirby->user(
|
||||
null,
|
||||
$kirby->option('api.allowImpersonation', false)
|
||||
);
|
||||
|
||||
throw new NotFoundException([
|
||||
return $user ?? throw new NotFoundException([
|
||||
'key' => 'user.undefined'
|
||||
]);
|
||||
}
|
||||
|
||||
// get a specific user by id
|
||||
if ($user = $kirby->user($id)) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
return $kirby->user($id) ?? throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $id
|
||||
|
|
|
@ -45,7 +45,7 @@ trait HasChildren
|
|||
*/
|
||||
public function children()
|
||||
{
|
||||
if (is_a($this->children, 'Kirby\Cms\Pages') === true) {
|
||||
if ($this->children instanceof Pages) {
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ trait HasChildren
|
|||
*/
|
||||
public function childrenAndDrafts()
|
||||
{
|
||||
if (is_a($this->childrenAndDrafts, 'Kirby\Cms\Pages') === true) {
|
||||
if ($this->childrenAndDrafts instanceof Pages) {
|
||||
return $this->childrenAndDrafts;
|
||||
}
|
||||
|
||||
|
@ -118,7 +118,7 @@ trait HasChildren
|
|||
*/
|
||||
public function drafts()
|
||||
{
|
||||
if (is_a($this->drafts, 'Kirby\Cms\Pages') === true) {
|
||||
if ($this->drafts instanceof Pages) {
|
||||
return $this->drafts;
|
||||
}
|
||||
|
||||
|
@ -217,9 +217,9 @@ trait HasChildren
|
|||
{
|
||||
if ($drafts === true) {
|
||||
return $this->childrenAndDrafts()->index($drafts);
|
||||
} else {
|
||||
return $this->children()->index();
|
||||
}
|
||||
|
||||
return $this->children()->index();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* HasFiles
|
||||
*
|
||||
|
@ -90,6 +92,11 @@ trait HasFiles
|
|||
return $this->$in()->first();
|
||||
}
|
||||
|
||||
// find by global UUID
|
||||
if (Uuid::is($filename, 'file') === true) {
|
||||
return Uuid::for($filename, $this->files())->model();
|
||||
}
|
||||
|
||||
if (strpos($filename, '/') !== false) {
|
||||
$path = dirname($filename);
|
||||
$filename = basename($filename);
|
||||
|
@ -111,7 +118,7 @@ trait HasFiles
|
|||
*/
|
||||
public function files()
|
||||
{
|
||||
if (is_a($this->files, 'Kirby\Cms\Files') === true) {
|
||||
if ($this->files instanceof Files) {
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,10 +23,7 @@ trait HasSiblings
|
|||
*/
|
||||
public function indexOf($collection = null): int
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
$collection ??= $this->siblingsCollection();
|
||||
return $collection->indexOf($this);
|
||||
}
|
||||
|
||||
|
@ -39,10 +36,7 @@ trait HasSiblings
|
|||
*/
|
||||
public function next($collection = null)
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
$collection ??= $this->siblingsCollection();
|
||||
return $collection->nth($this->indexOf($collection) + 1);
|
||||
}
|
||||
|
||||
|
@ -55,10 +49,7 @@ trait HasSiblings
|
|||
*/
|
||||
public function nextAll($collection = null)
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
$collection ??= $this->siblingsCollection();
|
||||
return $collection->slice($this->indexOf($collection) + 1);
|
||||
}
|
||||
|
||||
|
@ -71,10 +62,7 @@ trait HasSiblings
|
|||
*/
|
||||
public function prev($collection = null)
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
$collection ??= $this->siblingsCollection();
|
||||
return $collection->nth($this->indexOf($collection) - 1);
|
||||
}
|
||||
|
||||
|
@ -87,10 +75,7 @@ trait HasSiblings
|
|||
*/
|
||||
public function prevAll($collection = null)
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
$collection ??= $this->siblingsCollection();
|
||||
return $collection->slice(0, $this->indexOf($collection));
|
||||
}
|
||||
|
||||
|
@ -144,10 +129,7 @@ trait HasSiblings
|
|||
*/
|
||||
public function isFirst($collection = null): bool
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
$collection ??= $this->siblingsCollection();
|
||||
return $collection->first()->is($this);
|
||||
}
|
||||
|
||||
|
@ -160,10 +142,7 @@ trait HasSiblings
|
|||
*/
|
||||
public function isLast($collection = null): bool
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
$collection ??= $this->siblingsCollection();
|
||||
return $collection->last()->is($this);
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,18 @@ class Helpers
|
|||
public static function dump($variable, bool $echo = true): string
|
||||
{
|
||||
$kirby = App::instance();
|
||||
return ($kirby->component('dump'))($kirby, $variable, $echo);
|
||||
|
||||
if ($kirby->environment()->cli() === true) {
|
||||
$output = print_r($variable, true) . PHP_EOL;
|
||||
} else {
|
||||
$output = '<pre>' . print_r($variable, true) . '</pre>';
|
||||
}
|
||||
|
||||
if ($echo === true) {
|
||||
echo $output;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -53,29 +64,38 @@ class Helpers
|
|||
* @since 3.7.4
|
||||
*
|
||||
* @param \Closure $action Any action that may cause an error or warning
|
||||
* @param \Closure $handler Custom callback like for `set_error_handler()`;
|
||||
* the first argument is a return value override passed
|
||||
* by reference, the additional arguments come from
|
||||
* `set_error_handler()`; returning `false` activates
|
||||
* error handling by Whoops and/or PHP
|
||||
* @return mixed Return value of the `$action` closure, possibly overridden by `$handler`
|
||||
* @param \Closure $condition Closure that returns bool to determine if to
|
||||
* suppress an error, receives arguments for
|
||||
* `set_error_handler()`
|
||||
* @param mixed $fallback Value to return when error is suppressed
|
||||
* @return mixed Return value of the `$action` closure,
|
||||
* possibly overridden by `$fallback`
|
||||
*/
|
||||
public static function handleErrors(Closure $action, Closure $handler)
|
||||
public static function handleErrors(Closure $action, Closure $condition, $fallback = null)
|
||||
{
|
||||
$override = $oldHandler = null;
|
||||
$oldHandler = set_error_handler(function () use (&$override, &$oldHandler, $handler) {
|
||||
$handlerResult = $handler($override, ...func_get_args());
|
||||
$override = null;
|
||||
|
||||
if ($handlerResult === false) {
|
||||
$handler = set_error_handler(function () use (&$override, &$handler, $condition, $fallback) {
|
||||
// check if suppress condition is met
|
||||
$suppress = $condition(...func_get_args());
|
||||
|
||||
if ($suppress !== true) {
|
||||
// handle other warnings with Whoops if loaded
|
||||
if (is_callable($oldHandler) === true) {
|
||||
return $oldHandler(...func_get_args());
|
||||
if (is_callable($handler) === true) {
|
||||
return $handler(...func_get_args());
|
||||
}
|
||||
|
||||
// otherwise use the standard error handler
|
||||
return false; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// use fallback to override return for suppressed errors
|
||||
$override = $fallback;
|
||||
|
||||
if (is_callable($override) === true) {
|
||||
$override = $override();
|
||||
}
|
||||
|
||||
// no additional error handling
|
||||
return true;
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@ class Html extends \Kirby\Toolkit\Html
|
|||
* @param string|array $options Pass an array of attributes for the link tag or a media attribute string
|
||||
* @return string|null
|
||||
*/
|
||||
public static function css($url, $options = null): ?string
|
||||
public static function css($url, $options = null): string|null
|
||||
{
|
||||
if (is_array($url) === true) {
|
||||
$links = A::map($url, fn ($url) => static::css($url, $options));
|
||||
|
@ -83,7 +83,7 @@ class Html extends \Kirby\Toolkit\Html
|
|||
* @param string|array $options
|
||||
* @return string|null
|
||||
*/
|
||||
public static function js($url, $options = null): ?string
|
||||
public static function js($url, $options = null): string|null
|
||||
{
|
||||
if (is_array($url) === true) {
|
||||
$scripts = A::map($url, fn ($url) => static::js($url, $options));
|
||||
|
@ -121,7 +121,7 @@ class Html extends \Kirby\Toolkit\Html
|
|||
{
|
||||
// support for Kirby's file objects
|
||||
if (
|
||||
is_a($file, 'Kirby\Cms\File') === true &&
|
||||
$file instanceof File &&
|
||||
$file->extension() === 'svg'
|
||||
) {
|
||||
return $file->read();
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* The Ingredients class is the foundation for
|
||||
* `$kirby->urls()` and `$kirby->roots()` objects.
|
||||
|
@ -75,7 +77,7 @@ class Ingredients
|
|||
public static function bake(array $ingredients)
|
||||
{
|
||||
foreach ($ingredients as $name => $ingredient) {
|
||||
if (is_a($ingredient, 'Closure') === true) {
|
||||
if ($ingredient instanceof Closure) {
|
||||
$ingredients[$name] = $ingredient($ingredients);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ class Item
|
|||
{
|
||||
use HasSiblings;
|
||||
|
||||
public const ITEMS_CLASS = '\Kirby\Cms\Items';
|
||||
public const ITEMS_CLASS = Items::class;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
|
|
|
@ -17,7 +17,7 @@ use Exception;
|
|||
*/
|
||||
class Items extends Collection
|
||||
{
|
||||
public const ITEM_CLASS = '\Kirby\Cms\Item';
|
||||
public const ITEM_CLASS = Item::class;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
|
|
|
@ -212,6 +212,15 @@ class Language extends Model
|
|||
|
||||
$language = new static($props);
|
||||
|
||||
// trigger before hook
|
||||
$kirby->trigger(
|
||||
'language.create:before',
|
||||
[
|
||||
'input' => $props,
|
||||
'language' => $language
|
||||
]
|
||||
);
|
||||
|
||||
// validate the new language
|
||||
LanguageRules::create($language);
|
||||
|
||||
|
@ -222,7 +231,16 @@ class Language extends Model
|
|||
}
|
||||
|
||||
// update the main languages collection in the app instance
|
||||
App::instance()->languages(false)->append($language->code(), $language);
|
||||
$kirby->languages(false)->append($language->code(), $language);
|
||||
|
||||
// trigger after hook
|
||||
$kirby->trigger(
|
||||
'language.create:after',
|
||||
[
|
||||
'input' => $props,
|
||||
'language' => $language
|
||||
]
|
||||
);
|
||||
|
||||
return $language;
|
||||
}
|
||||
|
@ -242,6 +260,11 @@ class Language extends Model
|
|||
$code = $this->code();
|
||||
$isLast = $languages->count() === 1;
|
||||
|
||||
// trigger before hook
|
||||
$kirby->trigger('language.delete:before', [
|
||||
'language' => $this
|
||||
]);
|
||||
|
||||
if (F::remove($this->root()) !== true) {
|
||||
throw new Exception('The language could not be deleted');
|
||||
}
|
||||
|
@ -255,6 +278,11 @@ class Language extends Model
|
|||
// get the original language collection and remove the current language
|
||||
$kirby->languages(false)->remove($code);
|
||||
|
||||
// trigger after hook
|
||||
$kirby->trigger('language.delete:after', [
|
||||
'language' => $this
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -350,7 +378,7 @@ class Language extends Model
|
|||
|
||||
try {
|
||||
return Data::read($file);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Exception) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -365,9 +393,9 @@ class Language extends Model
|
|||
{
|
||||
if ($category !== null) {
|
||||
return $this->locale[$category] ?? $this->locale[LC_ALL] ?? null;
|
||||
} else {
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -456,7 +484,7 @@ class Language extends Model
|
|||
{
|
||||
try {
|
||||
$existingData = Data::read($this->root());
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
$existingData = [];
|
||||
}
|
||||
|
||||
|
@ -660,11 +688,15 @@ class Language extends Model
|
|||
// validate the updated language
|
||||
LanguageRules::update($updated);
|
||||
|
||||
// trigger before hook
|
||||
$kirby->trigger('language.update:before', [
|
||||
'language' => $this,
|
||||
'input' => $props
|
||||
]);
|
||||
|
||||
// convert the current default to a non-default language
|
||||
if ($updated->isDefault() === true) {
|
||||
if ($oldDefault = $kirby->defaultLanguage()) {
|
||||
$oldDefault->clone(['default' => false])->save();
|
||||
}
|
||||
$kirby->defaultLanguage()?->clone(['default' => false])->save();
|
||||
|
||||
$code = $this->code();
|
||||
$site = $kirby->site();
|
||||
|
@ -689,6 +721,13 @@ class Language extends Model
|
|||
// make sure the language is also updated in the Kirby language collection
|
||||
App::instance()->languages(false)->set($language->code(), $language);
|
||||
|
||||
// trigger after hook
|
||||
$kirby->trigger('language.update:after', [
|
||||
'newLanguage' => $language,
|
||||
'oldLanguage' => $this,
|
||||
'input' => $props
|
||||
]);
|
||||
|
||||
return $language;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -123,11 +123,11 @@ class LanguageRouter
|
|||
|
||||
if ($page = $route->page()) {
|
||||
return $route->action()->call($route, $language, $page, ...$route->arguments());
|
||||
} else {
|
||||
return $route->action()->call($route, $language, ...$route->arguments());
|
||||
}
|
||||
|
||||
return $route->action()->call($route, $language, ...$route->arguments());
|
||||
});
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
return $kirby->resolve($path, $language->code());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,11 @@ class LanguageRoutes
|
|||
'method' => 'ALL',
|
||||
'env' => 'site',
|
||||
'action' => function ($path = null) use ($language) {
|
||||
if ($result = $language->router()->call($path)) {
|
||||
$result = $language->router()->call($path);
|
||||
|
||||
// explicitly test for null as $result can
|
||||
// contain falsy values that should still be returned
|
||||
if ($result !== null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
|
|
@ -66,11 +66,7 @@ class Languages extends Collection
|
|||
*/
|
||||
public function default()
|
||||
{
|
||||
if ($language = $this->findBy('isDefault', true)) {
|
||||
return $language;
|
||||
} else {
|
||||
return $this->first();
|
||||
}
|
||||
return $this->findBy('isDefault', true) ?? $this->first();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -85,7 +81,7 @@ class Languages extends Collection
|
|||
$files = glob(App::instance()->root('languages') . '/*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$props = F::load($file);
|
||||
$props = F::load($file, allowOutput: false);
|
||||
|
||||
if (is_array($props) === true) {
|
||||
// inject the language code from the filename
|
||||
|
|
|
@ -17,7 +17,7 @@ class Layout extends Item
|
|||
{
|
||||
use HasMethods;
|
||||
|
||||
public const ITEMS_CLASS = '\Kirby\Cms\Layouts';
|
||||
public const ITEMS_CLASS = Layouts::class;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Content
|
||||
|
|
|
@ -19,7 +19,7 @@ class LayoutColumn extends Item
|
|||
{
|
||||
use HasMethods;
|
||||
|
||||
public const ITEMS_CLASS = '\Kirby\Cms\LayoutColumns';
|
||||
public const ITEMS_CLASS = LayoutColumns::class;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Blocks
|
||||
|
|
|
@ -14,5 +14,5 @@ namespace Kirby\Cms;
|
|||
*/
|
||||
class LayoutColumns extends Items
|
||||
{
|
||||
public const ITEM_CLASS = '\Kirby\Cms\LayoutColumn';
|
||||
public const ITEM_CLASS = LayoutColumn::class;
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ use Throwable;
|
|||
*/
|
||||
class Layouts extends Items
|
||||
{
|
||||
public const ITEM_CLASS = '\Kirby\Cms\Layout';
|
||||
public const ITEM_CLASS = Layout::class;
|
||||
|
||||
public static function factory(array $items = null, array $params = [])
|
||||
{
|
||||
|
@ -65,7 +65,7 @@ class Layouts extends Items
|
|||
if (empty($input) === false && is_array($input) === false) {
|
||||
try {
|
||||
$input = Data::decode($input, 'json');
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ class Loader
|
|||
* @param string $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function area(string $name): ?array
|
||||
public function area(string $name): array|null
|
||||
{
|
||||
return $this->areas()[$name] ?? null;
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ class Loader
|
|||
* @param string $name
|
||||
* @return \Closure|null
|
||||
*/
|
||||
public function component(string $name): ?Closure
|
||||
public function component(string $name): Closure|null
|
||||
{
|
||||
return $this->extension('components', $name);
|
||||
}
|
||||
|
@ -159,14 +159,13 @@ class Loader
|
|||
public function resolve($item)
|
||||
{
|
||||
if (is_string($item) === true) {
|
||||
if (F::extension($item) !== 'php') {
|
||||
$item = Data::read($item);
|
||||
} else {
|
||||
$item = require $item;
|
||||
}
|
||||
$item = match (F::extension($item)) {
|
||||
'php' => F::load($item, allowOutput: false),
|
||||
default => Data::read($item)
|
||||
};
|
||||
}
|
||||
|
||||
if (is_callable($item)) {
|
||||
if (is_callable($item) === true) {
|
||||
$item = $item($this->kirby);
|
||||
}
|
||||
|
||||
|
@ -206,7 +205,7 @@ class Loader
|
|||
// convert closure dropdowns to an array definition
|
||||
// otherwise they cannot be merged properly later
|
||||
foreach ($dropdowns as $key => $dropdown) {
|
||||
if (is_a($dropdown, 'Closure') === true) {
|
||||
if ($dropdown instanceof Closure) {
|
||||
$area['dropdowns'][$key] = [
|
||||
'options' => $dropdown
|
||||
];
|
||||
|
@ -222,7 +221,7 @@ class Loader
|
|||
* @param string $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function section(string $name): ?array
|
||||
public function section(string $name): array|null
|
||||
{
|
||||
return $this->resolve($this->extension('sections', $name));
|
||||
}
|
||||
|
|
|
@ -46,10 +46,10 @@ class Media
|
|||
// 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, render the error page
|
||||
return false;
|
||||
}
|
||||
|
||||
// don't leak the correct token, render the error page
|
||||
return false;
|
||||
}
|
||||
|
||||
// send the file to the browser
|
||||
|
@ -97,16 +97,17 @@ class Media
|
|||
{
|
||||
$kirby = App::instance();
|
||||
|
||||
// assets
|
||||
if (is_string($model) === true) {
|
||||
$root = $kirby->root('media') . '/assets/' . $model . '/' . $hash;
|
||||
// parent files for file model that already included hash
|
||||
} elseif (is_a($model, '\Kirby\Cms\File')) {
|
||||
$root = dirname($model->mediaRoot());
|
||||
// model files
|
||||
} else {
|
||||
$root = $model->mediaRoot() . '/' . $hash;
|
||||
}
|
||||
$root = match (true) {
|
||||
// assets
|
||||
is_string($model)
|
||||
=> $kirby->root('media') . '/assets/' . $model . '/' . $hash,
|
||||
// parent files for file model that already included hash
|
||||
$model instanceof File
|
||||
=> dirname($model->mediaRoot()),
|
||||
// model files
|
||||
default
|
||||
=> $model->mediaRoot() . '/' . $hash
|
||||
};
|
||||
|
||||
try {
|
||||
$thumb = $root . '/' . $filename;
|
||||
|
@ -117,21 +118,22 @@ class Media
|
|||
return false;
|
||||
}
|
||||
|
||||
if (is_string($model) === true) {
|
||||
$source = $kirby->root('index') . '/' . $model . '/' . $options['filename'];
|
||||
} else {
|
||||
$source = $model->file($options['filename'])->root();
|
||||
}
|
||||
$source = match (true) {
|
||||
is_string($model) === true
|
||||
=> $kirby->root('index') . '/' . $model . '/' . $options['filename'],
|
||||
default
|
||||
=> $model->file($options['filename'])->root()
|
||||
};
|
||||
|
||||
try {
|
||||
$kirby->thumb($source, $thumb, $options);
|
||||
F::remove($job);
|
||||
return Response::file($thumb);
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
F::remove($thumb);
|
||||
return Response::file($source);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ abstract class Model
|
|||
/**
|
||||
* Each model must return a unique id
|
||||
*
|
||||
* @return string|int
|
||||
* @return string|null
|
||||
*/
|
||||
public function id()
|
||||
{
|
||||
|
|
|
@ -67,7 +67,10 @@ abstract class ModelPermissions
|
|||
}
|
||||
|
||||
// check for a custom overall can method
|
||||
if (method_exists($this, 'can' . $action) === true && $this->{'can' . $action}() === false) {
|
||||
if (
|
||||
method_exists($this, 'can' . $action) === true &&
|
||||
$this->{'can' . $action}() === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -83,7 +86,10 @@ abstract class ModelPermissions
|
|||
return true;
|
||||
}
|
||||
|
||||
if (is_array($options) === true && A::isAssociative($options) === true) {
|
||||
if (
|
||||
is_array($options) === true &&
|
||||
A::isAssociative($options) === true
|
||||
) {
|
||||
return $options[$role] ?? $options['*'] ?? false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ use Kirby\Data\Data;
|
|||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Form\Form;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Uuid\Identifiable;
|
||||
use Kirby\Uuid\Uuid;
|
||||
use Kirby\Uuid\Uuids;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
|
@ -18,7 +21,7 @@ use Throwable;
|
|||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
abstract class ModelWithContent extends Model
|
||||
abstract class ModelWithContent extends Model implements Identifiable
|
||||
{
|
||||
/**
|
||||
* The content
|
||||
|
@ -85,35 +88,39 @@ abstract class ModelWithContent extends Model
|
|||
{
|
||||
// single language support
|
||||
if ($this->kirby()->multilang() === false) {
|
||||
if (is_a($this->content, 'Kirby\Cms\Content') === true) {
|
||||
if ($this->content instanceof Content) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
// don't normalize field keys (already handled by the `Data` class)
|
||||
return $this->content = new Content($this->readContent(), $this, false);
|
||||
|
||||
// multi language support
|
||||
} else {
|
||||
// only fetch from cache for the default language
|
||||
if ($languageCode === null && is_a($this->content, 'Kirby\Cms\Content') === true) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
// get the translation by code
|
||||
if ($translation = $this->translation($languageCode)) {
|
||||
// don't normalize field keys (already handled by the `ContentTranslation` class)
|
||||
$content = new Content($translation->content(), $this, false);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid language: ' . $languageCode);
|
||||
}
|
||||
|
||||
// only store the content for the current language
|
||||
if ($languageCode === null) {
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
// get the targeted language
|
||||
$language = $this->kirby()->language($languageCode);
|
||||
|
||||
// stop if the language does not exist
|
||||
if ($language === null) {
|
||||
throw new InvalidArgumentException('Invalid language: ' . $languageCode);
|
||||
}
|
||||
|
||||
// only fetch from cache for the current language
|
||||
if ($languageCode === null && $this->content instanceof Content) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
// get the translation by code
|
||||
$translation = $this->translation($language->code());
|
||||
|
||||
// don't normalize field keys (already handled by the `ContentTranslation` class)
|
||||
$content = new Content($translation->content(), $this, false);
|
||||
|
||||
// only store the content for the current language
|
||||
if ($languageCode === null) {
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -135,21 +142,21 @@ abstract class ModelWithContent extends Model
|
|||
if ($force === true) {
|
||||
if (empty($languageCode) === false) {
|
||||
return $directory . '/' . $filename . '.' . $languageCode . '.' . $extension;
|
||||
} else {
|
||||
return $directory . '/' . $filename . '.' . $extension;
|
||||
}
|
||||
|
||||
return $directory . '/' . $filename . '.' . $extension;
|
||||
}
|
||||
|
||||
// add and validate the language code in multi language mode
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
if ($language = $this->kirby()->languageCode($languageCode)) {
|
||||
return $directory . '/' . $filename . '.' . $language . '.' . $extension;
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid language: ' . $languageCode);
|
||||
}
|
||||
} else {
|
||||
return $directory . '/' . $filename . '.' . $extension;
|
||||
|
||||
throw new InvalidArgumentException('Invalid language: ' . $languageCode);
|
||||
}
|
||||
|
||||
return $directory . '/' . $filename . '.' . $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -165,11 +172,11 @@ abstract class ModelWithContent extends Model
|
|||
$files[] = $this->contentFile($code);
|
||||
}
|
||||
return $files;
|
||||
} else {
|
||||
return [
|
||||
$this->contentFile()
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
$this->contentFile()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -194,7 +201,7 @@ abstract class ModelWithContent extends Model
|
|||
* @internal
|
||||
* @return string|null
|
||||
*/
|
||||
public function contentFileDirectory(): ?string
|
||||
public function contentFileDirectory(): string|null
|
||||
{
|
||||
return $this->root();
|
||||
}
|
||||
|
@ -346,15 +353,15 @@ abstract class ModelWithContent extends Model
|
|||
try {
|
||||
$result = Str::query($query, [
|
||||
'kirby' => $this->kirby(),
|
||||
'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(),
|
||||
'site' => $this instanceof Site ? $this : $this->site(),
|
||||
'model' => $this,
|
||||
static::CLASS_ALIAS => $this
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($expect !== null && is_a($result, $expect) !== true) {
|
||||
if ($expect !== null && $result instanceof $expect === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -370,11 +377,16 @@ abstract class ModelWithContent extends Model
|
|||
*/
|
||||
public function readContent(string $languageCode = null): array
|
||||
{
|
||||
try {
|
||||
return Data::read($this->contentFile($languageCode));
|
||||
} catch (Throwable $e) {
|
||||
$file = $this->contentFile($languageCode);
|
||||
|
||||
// only if the content file really does not exist, it's ok
|
||||
// to return empty content. Otherwise this could lead to
|
||||
// content loss in case of file reading issues
|
||||
if (file_exists($file) === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Data::read($file);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -382,7 +394,7 @@ abstract class ModelWithContent extends Model
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
abstract public function root(): ?string;
|
||||
abstract public function root(): string|null;
|
||||
|
||||
/**
|
||||
* Stores the content on disk
|
||||
|
@ -397,9 +409,9 @@ abstract class ModelWithContent extends Model
|
|||
{
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
return $this->saveTranslation($data, $languageCode, $overwrite);
|
||||
} else {
|
||||
return $this->saveContent($data, $overwrite);
|
||||
}
|
||||
|
||||
return $this->saveContent($data, $overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -457,6 +469,11 @@ abstract class ModelWithContent extends Model
|
|||
}
|
||||
}
|
||||
|
||||
// remove UUID for non-default languages
|
||||
if (Uuids::enabled() === true && isset($content['uuid']) === true) {
|
||||
$content['uuid'] = null;
|
||||
}
|
||||
|
||||
// merge the translation with the new data
|
||||
$translation->update($content, true);
|
||||
}
|
||||
|
@ -514,10 +531,11 @@ abstract class ModelWithContent extends Model
|
|||
*
|
||||
* @param string|null $template Template string or `null` to use the model ID
|
||||
* @param array $data
|
||||
* @param string $fallback Fallback for tokens in the template that cannot be replaced
|
||||
* @param string|null $fallback Fallback for tokens in the template that cannot be replaced
|
||||
* (`null` to keep the original token)
|
||||
* @return string
|
||||
*/
|
||||
public function toSafeString(string $template = null, array $data = [], string $fallback = ''): string
|
||||
public function toSafeString(string $template = null, array $data = [], string|null $fallback = ''): string
|
||||
{
|
||||
return $this->toString($template, $data, $fallback, 'safeTemplate');
|
||||
}
|
||||
|
@ -527,11 +545,12 @@ abstract class ModelWithContent extends Model
|
|||
*
|
||||
* @param string|null $template Template string or `null` to use the model ID
|
||||
* @param array $data
|
||||
* @param string $fallback Fallback for tokens in the template that cannot be replaced
|
||||
* @param string|null $fallback Fallback for tokens in the template that cannot be replaced
|
||||
* (`null` to keep the original token)
|
||||
* @param string $handler For internal use
|
||||
* @return string
|
||||
*/
|
||||
public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string
|
||||
public function toString(string $template = null, array $data = [], string|null $fallback = '', string $handler = 'template'): string
|
||||
{
|
||||
if ($template === null) {
|
||||
return $this->id() ?? '';
|
||||
|
@ -543,7 +562,7 @@ abstract class ModelWithContent extends Model
|
|||
|
||||
$result = Str::$handler($template, array_replace([
|
||||
'kirby' => $this->kirby(),
|
||||
'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(),
|
||||
'site' => $this instanceof Site ? $this : $this->site(),
|
||||
'model' => $this,
|
||||
static::CLASS_ALIAS => $this,
|
||||
], $data), ['fallback' => $fallback]);
|
||||
|
@ -560,7 +579,11 @@ abstract class ModelWithContent extends Model
|
|||
*/
|
||||
public function translation(string $languageCode = null)
|
||||
{
|
||||
return $this->translations()->find($languageCode ?? $this->kirby()->language()->code());
|
||||
if ($language = $this->kirby()->language($languageCode)) {
|
||||
return $this->translations()->find($language->code());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -617,16 +640,19 @@ abstract class ModelWithContent extends Model
|
|||
|
||||
$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);
|
||||
|
||||
// update model in siblings collection
|
||||
$model->siblings()->add($model);
|
||||
|
||||
return $model;
|
||||
return $model->save($strings, $languageCode, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model's UUID
|
||||
* @since 3.8.0
|
||||
*/
|
||||
public function uuid(): Uuid|null
|
||||
{
|
||||
return Uuid::for($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level data writer method
|
||||
* to store the given data on disk or anywhere else
|
||||
|
@ -643,59 +669,4 @@ abstract class ModelWithContent extends Model
|
|||
$this->contentFileData($data, $languageCode)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deprecated!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the panel icon definition
|
||||
*
|
||||
* @deprecated 3.6.0 Use `->panel()->image()` instead
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @param array|null $params
|
||||
* @return array|null
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelIcon(array $params = null): ?array
|
||||
{
|
||||
Helpers::deprecated('Cms\ModelWithContent::panelIcon() has been deprecated and will be removed in Kirby 3.8.0. Use $model->panel()->image() instead.');
|
||||
return $this->panel()->image($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.6.0 Use `->panel()->image()` instead
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @param string|array|false|null $settings
|
||||
* @return array|null
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelImage($settings = null): ?array
|
||||
{
|
||||
Helpers::deprecated('Cms\ModelWithContent::panelImage() has been deprecated and will be removed in Kirby 3.8.0. Use $model->panel()->image() instead.');
|
||||
return $this->panel()->image($settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all actions
|
||||
* that can be performed in the Panel
|
||||
* This also checks for the lock status
|
||||
*
|
||||
* @deprecated 3.6.0 Use `->panel()->options()` instead
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @param array $unlock An array of options that will be force-unlocked
|
||||
* @return array
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelOptions(array $unlock = []): array
|
||||
{
|
||||
Helpers::deprecated('Cms\ModelWithContent::panelOptions() has been deprecated and will be removed in Kirby 3.8.0. Use $model->panel()->options() instead.');
|
||||
return $this->panel()->options($unlock);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,10 @@ namespace Kirby\Cms;
|
|||
*/
|
||||
class Nest
|
||||
{
|
||||
/**
|
||||
* @param $data
|
||||
* @param null $parent
|
||||
* @return mixed
|
||||
*/
|
||||
public static function create($data, $parent = null)
|
||||
{
|
||||
public static function create(
|
||||
$data,
|
||||
object|null $parent = null
|
||||
): NestCollection|NestObject|Field {
|
||||
if (is_scalar($data) === true) {
|
||||
return new Field($parent, $data, $data);
|
||||
}
|
||||
|
@ -39,10 +36,12 @@ class Nest
|
|||
}
|
||||
}
|
||||
|
||||
if (is_int(key($data))) {
|
||||
$key = key($data);
|
||||
|
||||
if ($key === null || is_int($key) === true) {
|
||||
return new NestCollection($result);
|
||||
} else {
|
||||
return new NestObject($result);
|
||||
}
|
||||
|
||||
return new NestObject($result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,6 @@ class NestCollection extends BaseCollection
|
|||
* Converts all objects in the collection
|
||||
* to an array. This can also take a callback
|
||||
* function to further modify the array result.
|
||||
*
|
||||
* @param \Closure|null $map
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Closure $map = null): array
|
||||
{
|
||||
|
|
|
@ -17,20 +17,21 @@ class NestObject extends Obj
|
|||
{
|
||||
/**
|
||||
* Converts the object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ((array)$this as $key => $value) {
|
||||
if (is_a($value, 'Kirby\Cms\Field') === true) {
|
||||
if ($value instanceof Field) {
|
||||
$result[$key] = $value->value();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_object($value) === true && method_exists($value, 'toArray')) {
|
||||
if (
|
||||
is_object($value) === true &&
|
||||
method_exists($value, 'toArray')
|
||||
) {
|
||||
$result[$key] = $value->toArray();
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
|
@ -217,9 +218,9 @@ class Page extends ModelWithContent
|
|||
{
|
||||
if ($relative === true) {
|
||||
return 'pages/' . $this->panel()->id();
|
||||
} else {
|
||||
return $this->kirby()->url('api') . '/pages/' . $this->panel()->id();
|
||||
}
|
||||
|
||||
return $this->kirby()->url('api') . '/pages/' . $this->panel()->id();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -229,7 +230,7 @@ class Page extends ModelWithContent
|
|||
*/
|
||||
public function blueprint()
|
||||
{
|
||||
if (is_a($this->blueprint, 'Kirby\Cms\PageBlueprint') === true) {
|
||||
if ($this->blueprint instanceof PageBlueprint) {
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
|
@ -242,7 +243,7 @@ class Page extends ModelWithContent
|
|||
* @param string|null $inSection
|
||||
* @return array
|
||||
*/
|
||||
public function blueprints(?string $inSection = null): array
|
||||
public function blueprints(string|null $inSection = null): array
|
||||
{
|
||||
if ($inSection !== null) {
|
||||
return $this->blueprint()->section($inSection)->blueprints();
|
||||
|
@ -272,7 +273,7 @@ class Page extends ModelWithContent
|
|||
'name' => basename($props['name']),
|
||||
'title' => $props['title'],
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
// skip invalid blueprints
|
||||
}
|
||||
}
|
||||
|
@ -307,7 +308,7 @@ class Page extends ModelWithContent
|
|||
* @param string|null $languageCode
|
||||
* @return array
|
||||
*/
|
||||
public function contentFileData(array $data, ?string $languageCode = null): array
|
||||
public function contentFileData(array $data, string|null $languageCode = null): array
|
||||
{
|
||||
return A::prepend($data, [
|
||||
'title' => $data['title'] ?? null,
|
||||
|
@ -323,7 +324,7 @@ class Page extends ModelWithContent
|
|||
* @param string|null $languageCode
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileName(?string $languageCode = null): string
|
||||
public function contentFileName(string|null $languageCode = null): string
|
||||
{
|
||||
return $this->intendedTemplate()->name();
|
||||
}
|
||||
|
@ -361,7 +362,7 @@ class Page extends ModelWithContent
|
|||
|
||||
foreach ($controllerData as $key => $value) {
|
||||
if (array_key_exists($key, $classes) === true) {
|
||||
if (is_a($value, $classes[$key]) === true) {
|
||||
if ($value instanceof $classes[$key]) {
|
||||
$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] . '"');
|
||||
|
@ -399,9 +400,9 @@ class Page extends ModelWithContent
|
|||
|
||||
if ($this->num() !== null) {
|
||||
return $this->dirname = $this->num() . Dir::$numSeparator . $this->uid();
|
||||
} else {
|
||||
return $this->dirname = $this->uid();
|
||||
}
|
||||
|
||||
return $this->dirname = $this->uid();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -423,9 +424,9 @@ class Page extends ModelWithContent
|
|||
|
||||
if ($parent = $this->parent()) {
|
||||
return $this->diruri = $parent->diruri() . '/' . $dirname;
|
||||
} else {
|
||||
return $this->diruri = $dirname;
|
||||
}
|
||||
|
||||
return $this->diruri = $dirname;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -545,7 +546,7 @@ class Page extends ModelWithContent
|
|||
*/
|
||||
public function is($page): bool
|
||||
{
|
||||
if (is_a($page, 'Kirby\Cms\Page') === false) {
|
||||
if ($page instanceof self === false) {
|
||||
if (is_string($page) === false) {
|
||||
return false;
|
||||
}
|
||||
|
@ -553,7 +554,7 @@ class Page extends ModelWithContent
|
|||
$page = $this->kirby()->page($page);
|
||||
}
|
||||
|
||||
if (is_a($page, 'Kirby\Cms\Page') === false) {
|
||||
if ($page instanceof self === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -567,10 +568,8 @@ class Page extends ModelWithContent
|
|||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
if ($page = $this->site()->page()) {
|
||||
if ($page->is($this) === true) {
|
||||
return true;
|
||||
}
|
||||
if ($this->site()->page()?->is($this) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -625,7 +624,7 @@ class Page extends ModelWithContent
|
|||
}
|
||||
|
||||
// check for a custom ignore rule
|
||||
if (is_a($ignore, 'Closure') === true) {
|
||||
if ($ignore instanceof Closure) {
|
||||
if ($ignore($this) === true) {
|
||||
return false;
|
||||
}
|
||||
|
@ -649,11 +648,7 @@ class Page extends ModelWithContent
|
|||
*/
|
||||
public function isChildOf($parent): bool
|
||||
{
|
||||
if ($parentObj = $this->parent()) {
|
||||
return $parentObj->is($parent);
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->parent()?->is($parent) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -754,10 +749,8 @@ class Page extends ModelWithContent
|
|||
return true;
|
||||
}
|
||||
|
||||
if ($page = $this->site()->page()) {
|
||||
if ($page->parents()->has($this->id()) === true) {
|
||||
return true;
|
||||
}
|
||||
if ($this->site()->page()?->parents()->has($this->id()) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -784,11 +777,7 @@ class Page extends ModelWithContent
|
|||
|
||||
$template = $this->intendedTemplate()->name();
|
||||
|
||||
if (isset($readable[$template]) === true) {
|
||||
return $readable[$template];
|
||||
}
|
||||
|
||||
return $readable[$template] = $this->permissions()->can('read');
|
||||
return $readable[$template] ??= $this->permissions()->can('read');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -870,7 +859,7 @@ class Page extends ModelWithContent
|
|||
if ($class = (static::$models[$name] ?? null)) {
|
||||
$object = new $class($props);
|
||||
|
||||
if (is_a($object, 'Kirby\Cms\Page') === true) {
|
||||
if ($object instanceof self) {
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
@ -900,7 +889,7 @@ class Page extends ModelWithContent
|
|||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function num(): ?int
|
||||
public function num(): int|null
|
||||
{
|
||||
return $this->num;
|
||||
}
|
||||
|
@ -931,13 +920,9 @@ class Page extends ModelWithContent
|
|||
* @internal
|
||||
* @return string|null
|
||||
*/
|
||||
public function parentId(): ?string
|
||||
public function parentId(): string|null
|
||||
{
|
||||
if ($parent = $this->parent()) {
|
||||
return $parent->id();
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->parent()?->id();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -971,6 +956,15 @@ class Page extends ModelWithContent
|
|||
return $parents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the permanent URL to the page using its UUID
|
||||
* @since 3.8.0
|
||||
*/
|
||||
public function permalink(): string|null
|
||||
{
|
||||
return $this->uuid()?->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the permissions object for this page
|
||||
*
|
||||
|
@ -987,7 +981,7 @@ class Page extends ModelWithContent
|
|||
* @internal
|
||||
* @return string|null
|
||||
*/
|
||||
public function previewUrl(): ?string
|
||||
public function previewUrl(): string|null
|
||||
{
|
||||
$preview = $this->blueprint()->preview();
|
||||
|
||||
|
@ -1066,9 +1060,24 @@ class Page extends ModelWithContent
|
|||
|
||||
$kirby->data = $this->controller($data, $contentType);
|
||||
|
||||
// trigger before hook and apply for `data`
|
||||
$kirby->data = $kirby->apply('page.render:before', [
|
||||
'contentType' => $contentType,
|
||||
'data' => $kirby->data,
|
||||
'page' => $this
|
||||
], 'data');
|
||||
|
||||
// render the page
|
||||
$html = $template->render($kirby->data);
|
||||
|
||||
// trigger after hook and apply for `html`
|
||||
$html = $kirby->apply('page.render:after', [
|
||||
'contentType' => $contentType,
|
||||
'data' => $kirby->data,
|
||||
'html' => $html,
|
||||
'page' => $this
|
||||
], 'html');
|
||||
|
||||
// cache the result
|
||||
$response = $kirby->response();
|
||||
if ($cache !== null && $response->cache() === true) {
|
||||
|
@ -1411,9 +1420,9 @@ class Page extends ModelWithContent
|
|||
if ($this->kirby()->multilang() === true) {
|
||||
if (is_string($options) === true) {
|
||||
return $this->urlForLanguage($options);
|
||||
} else {
|
||||
return $this->urlForLanguage(null, $options);
|
||||
}
|
||||
|
||||
return $this->urlForLanguage(null, $options);
|
||||
}
|
||||
|
||||
if ($options !== null) {
|
||||
|
@ -1431,9 +1440,9 @@ class Page extends ModelWithContent
|
|||
if ($parent = $this->parent()) {
|
||||
if ($parent->isHomePage() === true) {
|
||||
return $this->url = $this->kirby()->url('base') . '/' . $parent->uid() . '/' . $this->uid();
|
||||
} else {
|
||||
return $this->url = $this->parent()->url() . '/' . $this->uid();
|
||||
}
|
||||
|
||||
return $this->url = $this->parent()->url() . '/' . $this->uid();
|
||||
}
|
||||
|
||||
return $this->url = $this->kirby()->url('base') . '/' . $this->uid();
|
||||
|
@ -1460,104 +1469,11 @@ class Page extends ModelWithContent
|
|||
if ($parent = $this->parent()) {
|
||||
if ($parent->isHomePage() === true) {
|
||||
return $this->url = $this->site()->urlForLanguage($language) . '/' . $parent->slug($language) . '/' . $this->slug($language);
|
||||
} else {
|
||||
return $this->url = $this->parent()->urlForLanguage($language) . '/' . $this->slug($language);
|
||||
}
|
||||
|
||||
return $this->url = $this->parent()->urlForLanguage($language) . '/' . $this->slug($language);
|
||||
}
|
||||
|
||||
return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deprecated!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides a kirbytag or markdown
|
||||
* tag for the page, which will be
|
||||
* used in the panel, when the page
|
||||
* gets dragged onto a textarea
|
||||
*
|
||||
* @deprecated 3.6.0 Use `->panel()->dragText()` instead
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $type (null|auto|kirbytext|markdown)
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function dragText(string $type = null): string
|
||||
{
|
||||
Helpers::deprecated('Cms\Page::dragText() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->dragText() instead.');
|
||||
return $this->panel()->dragText($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the escaped Id, which is
|
||||
* used in the panel to make routing work properly
|
||||
*
|
||||
* @deprecated 3.6.0 Use `->panel()->id()` instead
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelId(): string
|
||||
{
|
||||
Helpers::deprecated('Cms\Page::panelId() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->id() instead.');
|
||||
return $this->panel()->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @deprecated 3.6.0 Use `->panel()->path()` instead
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
Helpers::deprecated('Cms\Page::panelPath() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->path() instead.');
|
||||
return $this->panel()->path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the response data for page pickers
|
||||
* and page fields
|
||||
*
|
||||
* @deprecated 3.6.0 Use `->panel()->pickerData()` instead
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @param array|null $params
|
||||
* @return array
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelPickerData(array $params = []): array
|
||||
{
|
||||
Helpers::deprecated('Cms\Page::panelPickerData() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->pickerData() instead.');
|
||||
return $this->panel()->pickerData($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @deprecated 3.6.0 Use `->panel()->url()` instead
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelUrl(bool $relative = false): string
|
||||
{
|
||||
Helpers::deprecated('Cms\Page::panelUrl() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->url() instead.');
|
||||
return $this->panel()->url($relative);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ use Kirby\Form\Form;
|
|||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Uuid\Uuid;
|
||||
use Kirby\Uuid\Uuids;
|
||||
|
||||
/**
|
||||
* PageActions
|
||||
|
@ -25,6 +27,75 @@ use Kirby\Toolkit\Str;
|
|||
*/
|
||||
trait PageActions
|
||||
{
|
||||
/**
|
||||
* Adapts necessary modifications which page uuid, page slug and files uuid
|
||||
* of copy objects for single or multilang environments
|
||||
*/
|
||||
protected function adaptCopy(Page $copy, bool $files = false, bool $children = false): Page
|
||||
{
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
foreach ($this->kirby()->languages() as $language) {
|
||||
// overwrite with new UUID for the page and files
|
||||
// for default language (remove old, add new)
|
||||
if (
|
||||
Uuids::enabled() === true &&
|
||||
$language->isDefault() === true
|
||||
) {
|
||||
$copy = $copy->save(['uuid' => Uuid::generate()], $language->code());
|
||||
|
||||
// regenerate UUIDs of page files
|
||||
if ($files !== false) {
|
||||
foreach ($copy->files() as $file) {
|
||||
$file->save(['uuid' => Uuid::generate()], $language->code());
|
||||
}
|
||||
}
|
||||
|
||||
// regenerate UUIDs of all page children
|
||||
if ($children !== false) {
|
||||
foreach ($copy->index(true) as $child) {
|
||||
// always adapt files of subpages as they are currently always copied;
|
||||
// but don't adapt children because we already operate on the index
|
||||
$this->adaptCopy($child, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all translated slugs
|
||||
if (
|
||||
$language->isDefault() === false &&
|
||||
$copy->translation($language)->exists() === true
|
||||
) {
|
||||
$copy = $copy->save(['slug' => null], $language->code());
|
||||
}
|
||||
}
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
// overwrite with new UUID for the page and files (remove old, add new)
|
||||
if (Uuids::enabled() === true) {
|
||||
$copy = $copy->save(['uuid' => Uuid::generate()]);
|
||||
|
||||
// regenerate UUIDs of page files
|
||||
if ($files !== false) {
|
||||
foreach ($copy->files() as $file) {
|
||||
$file->save(['uuid' => Uuid::generate()]);
|
||||
}
|
||||
}
|
||||
|
||||
// regenerate UUIDs of all page children
|
||||
if ($children !== false) {
|
||||
foreach ($copy->index(true) as $child) {
|
||||
// always adapt files of subpages as they are currently always copied;
|
||||
// but don't adapt children because we already operate on the index
|
||||
$this->adaptCopy($child, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the sorting number.
|
||||
* The sorting number must already be correct
|
||||
|
@ -88,10 +159,8 @@ trait PageActions
|
|||
// in multi-language installations the slug for the non-default
|
||||
// languages is stored in the text file. The changeSlugForLanguage
|
||||
// method takes care of that.
|
||||
if ($language = $this->kirby()->language($languageCode)) {
|
||||
if ($language->isDefault() === false) {
|
||||
return $this->changeSlugForLanguage($slug, $languageCode);
|
||||
}
|
||||
if ($this->kirby()->language($languageCode)?->isDefault() === false) {
|
||||
return $this->changeSlugForLanguage($slug, $languageCode);
|
||||
}
|
||||
|
||||
// if the slug stays exactly the same,
|
||||
|
@ -108,11 +177,12 @@ trait PageActions
|
|||
'root' => null
|
||||
]);
|
||||
|
||||
// clear UUID cache recursively (for children and files as well)
|
||||
$oldPage->uuid()?->clear(true);
|
||||
|
||||
if ($oldPage->exists() === true) {
|
||||
// remove the lock of the old page
|
||||
if ($lock = $oldPage->lock()) {
|
||||
$lock->remove();
|
||||
}
|
||||
$oldPage->lock()?->remove();
|
||||
|
||||
// actually move stuff on disk
|
||||
if (Dir::move($oldPage->root(), $newPage->root()) !== true) {
|
||||
|
@ -182,16 +252,12 @@ trait PageActions
|
|||
*/
|
||||
public function changeStatus(string $status, int $position = null)
|
||||
{
|
||||
switch ($status) {
|
||||
case 'draft':
|
||||
return $this->changeStatusToDraft();
|
||||
case 'listed':
|
||||
return $this->changeStatusToListed($position);
|
||||
case 'unlisted':
|
||||
return $this->changeStatusToUnlisted();
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid status: ' . $status);
|
||||
}
|
||||
return match ($status) {
|
||||
'draft' => $this->changeStatusToDraft(),
|
||||
'listed' => $this->changeStatusToListed($position),
|
||||
'unlisted' => $this->changeStatusToUnlisted(),
|
||||
default => throw new InvalidArgumentException('Invalid status: ' . $status)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -437,14 +503,8 @@ trait PageActions
|
|||
|
||||
$copy = $parentModel->clone()->findPageOrDraft($slug);
|
||||
|
||||
// remove all translated slugs
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
foreach ($this->kirby()->languages() as $language) {
|
||||
if ($language->isDefault() === false && $copy->translation($language)->exists() === true) {
|
||||
$copy = $copy->save(['slug' => null], $language->code());
|
||||
}
|
||||
}
|
||||
}
|
||||
// normalize copy object
|
||||
$copy = $this->adaptCopy($copy, $files, $children);
|
||||
|
||||
// add copy to siblings
|
||||
static::updateParentCollections($copy, 'append', $parentModel);
|
||||
|
@ -465,13 +525,19 @@ trait PageActions
|
|||
$props['template'] = $props['model'] = strtolower($props['template'] ?? 'default');
|
||||
$props['isDraft'] = ($props['draft'] ?? true);
|
||||
|
||||
// make sure that a UUID gets generated and
|
||||
// added to content right away
|
||||
$props['content'] ??= [];
|
||||
|
||||
if (Uuids::enabled() === true) {
|
||||
$props['content']['uuid'] ??= Uuid::generate();
|
||||
}
|
||||
|
||||
// create a temporary page object
|
||||
$page = Page::factory($props);
|
||||
|
||||
// create a form for the page
|
||||
$form = Form::for($page, [
|
||||
'values' => $props['content'] ?? []
|
||||
]);
|
||||
$form = Form::for($page, ['values' => $props['content']]);
|
||||
|
||||
// inject the content
|
||||
$page = $page->clone(['content' => $form->strings(true)]);
|
||||
|
@ -593,6 +659,9 @@ trait PageActions
|
|||
public function delete(bool $force = false): bool
|
||||
{
|
||||
return $this->commit('delete', ['page' => $this, 'force' => $force], function ($page, $force) {
|
||||
// clear UUID cache
|
||||
$page->uuid()?->clear();
|
||||
|
||||
// delete all files individually
|
||||
foreach ($page->files() as $file) {
|
||||
$file->delete();
|
||||
|
@ -765,9 +834,9 @@ trait PageActions
|
|||
foreach ($sorted as $key => $id) {
|
||||
if ($id === $this->id()) {
|
||||
continue;
|
||||
} elseif ($sibling = $siblings->get($id)) {
|
||||
$sibling->changeNum($key + 1);
|
||||
}
|
||||
|
||||
$siblings->get($id)?->changeNum($key + 1);
|
||||
}
|
||||
|
||||
$parent = $this->parentModel();
|
||||
|
@ -803,6 +872,25 @@ trait PageActions
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the content on disk
|
||||
*
|
||||
* @internal
|
||||
* @param array|null $data
|
||||
* @param string|null $languageCode
|
||||
* @param bool $overwrite
|
||||
* @return static
|
||||
*/
|
||||
public function save(array $data = null, string $languageCode = null, bool $overwrite = false)
|
||||
{
|
||||
$page = parent::save($data, $languageCode, $overwrite);
|
||||
|
||||
// overwrite the updated page in the parent collection
|
||||
static::updateParentCollections($page, 'set');
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page from listed or
|
||||
* unlisted to draft.
|
||||
|
@ -862,7 +950,10 @@ trait PageActions
|
|||
$page = parent::update($input, $languageCode, $validate);
|
||||
|
||||
// if num is created from page content, update num on content update
|
||||
if ($page->isListed() === true && in_array($page->blueprint()->num(), ['zero', 'default']) === false) {
|
||||
if (
|
||||
$page->isListed() === true &&
|
||||
in_array($page->blueprint()->num(), ['zero', 'default']) === false
|
||||
) {
|
||||
$page = $page->changeNum($page->createNum());
|
||||
}
|
||||
|
||||
|
|
|
@ -79,11 +79,7 @@ class PageBlueprint extends Blueprint
|
|||
'sort' => 'default',
|
||||
];
|
||||
|
||||
if (isset($aliases[$num]) === true) {
|
||||
return $aliases[$num];
|
||||
}
|
||||
|
||||
return $num;
|
||||
return $aliases[$num] ?? $num;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -144,9 +140,7 @@ class PageBlueprint extends Blueprint
|
|||
}
|
||||
|
||||
// also make sure to have the text field set
|
||||
if (isset($status[$key]['text']) === false) {
|
||||
$status[$key]['text'] = null;
|
||||
}
|
||||
$status[$key]['text'] ??= null;
|
||||
|
||||
// translate text and label if necessary
|
||||
$status[$key]['label'] = $this->i18n($status[$key]['label'], $status[$key]['label']);
|
||||
|
|
|
@ -39,7 +39,7 @@ class PagePermissions extends ModelPermissions
|
|||
*/
|
||||
protected function canChangeTemplate(): bool
|
||||
{
|
||||
if ($this->model->isHomeOrErrorPage() === true) {
|
||||
if ($this->model->isErrorPage() === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -86,11 +86,7 @@ class PagePicker extends Picker
|
|||
return $this->parent();
|
||||
}
|
||||
|
||||
if ($items = $this->items()) {
|
||||
return $items->parent();
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->items()?->parent();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -101,14 +97,14 @@ class PagePicker extends Picker
|
|||
* @param \Kirby\Cms\Site|\Kirby\Cms\Page|null
|
||||
* @return array|null
|
||||
*/
|
||||
public function modelToArray($model = null): ?array
|
||||
public function modelToArray($model = null): array|null
|
||||
{
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// the selected model is the site. there's nothing above
|
||||
if (is_a($model, 'Kirby\Cms\Site') === true) {
|
||||
if ($model instanceof Site) {
|
||||
return [
|
||||
'id' => null,
|
||||
'parent' => null,
|
||||
|
@ -200,13 +196,13 @@ class PagePicker extends Picker
|
|||
// help mitigate some typical query usage issues
|
||||
// by converting site and page objects to proper
|
||||
// pages by returning their children
|
||||
if (is_a($items, 'Kirby\Cms\Site') === true) {
|
||||
$items = $items->children();
|
||||
} elseif (is_a($items, 'Kirby\Cms\Page') === true) {
|
||||
$items = $items->children();
|
||||
} elseif (is_a($items, 'Kirby\Cms\Pages') === false) {
|
||||
throw new InvalidArgumentException('Your query must return a set of pages');
|
||||
}
|
||||
$items = match (true) {
|
||||
$items instanceof Site,
|
||||
$items instanceof Page => $items->children(),
|
||||
$items instanceof Pages => $items,
|
||||
|
||||
default => throw new InvalidArgumentException('Your query must return a set of pages')
|
||||
};
|
||||
|
||||
return $this->itemsForQuery = $items;
|
||||
}
|
||||
|
@ -238,11 +234,7 @@ class PagePicker extends Picker
|
|||
public function start()
|
||||
{
|
||||
if (empty($this->options['query']) === false) {
|
||||
if ($items = $this->itemsForQuery()) {
|
||||
return $items->parent();
|
||||
}
|
||||
|
||||
return $this->site;
|
||||
return $this->itemsForQuery()?->parent() ?? $this->site;
|
||||
}
|
||||
|
||||
return $this->site;
|
||||
|
|
|
@ -61,26 +61,22 @@ class PageRules
|
|||
$siblings = $page->parentModel()->children();
|
||||
$drafts = $page->parentModel()->drafts();
|
||||
|
||||
if ($duplicate = $siblings->find($slug)) {
|
||||
if ($duplicate->is($page) === false) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.duplicate',
|
||||
'data' => [
|
||||
'slug' => $slug
|
||||
]
|
||||
]);
|
||||
}
|
||||
if ($siblings->find($slug)?->is($page) === false) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.duplicate',
|
||||
'data' => [
|
||||
'slug' => $slug
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($duplicate = $drafts->find($slug)) {
|
||||
if ($duplicate->is($page) === false) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.draft.duplicate',
|
||||
'data' => [
|
||||
'slug' => $slug
|
||||
]
|
||||
]);
|
||||
}
|
||||
if ($drafts->find($slug)?->is($page) === false) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.draft.duplicate',
|
||||
'data' => [
|
||||
'slug' => $slug
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -101,16 +97,12 @@ class PageRules
|
|||
throw new InvalidArgumentException(['key' => 'page.status.invalid']);
|
||||
}
|
||||
|
||||
switch ($status) {
|
||||
case 'draft':
|
||||
return static::changeStatusToDraft($page);
|
||||
case 'listed':
|
||||
return static::changeStatusToListed($page, $position);
|
||||
case 'unlisted':
|
||||
return static::changeStatusToUnlisted($page);
|
||||
default:
|
||||
throw new InvalidArgumentException(['key' => 'page.status.invalid']);
|
||||
}
|
||||
return match ($status) {
|
||||
'draft' => static::changeStatusToDraft($page),
|
||||
'listed' => static::changeStatusToListed($page, $position),
|
||||
'unlisted' => static::changeStatusToUnlisted($page),
|
||||
default => throw new InvalidArgumentException(['key' => 'page.status.invalid'])
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -122,9 +122,9 @@ trait PageSiblings
|
|||
{
|
||||
if ($this->isDraft() === true) {
|
||||
return $this->parentModel()->drafts();
|
||||
} else {
|
||||
return $this->parentModel()->children();
|
||||
}
|
||||
|
||||
return $this->parentModel()->children();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Uuid\HasUuids;
|
||||
|
||||
/**
|
||||
* The `$pages` object refers to a
|
||||
|
@ -22,6 +23,8 @@ use Kirby\Exception\InvalidArgumentException;
|
|||
*/
|
||||
class Pages extends Collection
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
/**
|
||||
* Cache for the index only listed and unlisted pages
|
||||
*
|
||||
|
@ -57,15 +60,18 @@ class Pages extends Collection
|
|||
$site = App::instance()->site();
|
||||
|
||||
// add a pages collection
|
||||
if (is_a($object, self::class) === true) {
|
||||
if ($object instanceof self) {
|
||||
$this->data = array_merge($this->data, $object->data);
|
||||
|
||||
// add a page by id
|
||||
} elseif (is_string($object) === true && $page = $site->find($object)) {
|
||||
} elseif (
|
||||
is_string($object) === true &&
|
||||
$page = $site->find($object)
|
||||
) {
|
||||
$this->__set($page->id(), $page);
|
||||
|
||||
// add a page object
|
||||
} elseif (is_a($object, 'Kirby\Cms\Page') === true) {
|
||||
} elseif ($object instanceof Page) {
|
||||
$this->__set($object->id(), $object);
|
||||
|
||||
// give a useful error message on invalid input;
|
||||
|
@ -157,7 +163,7 @@ class Pages extends Collection
|
|||
$children = new static([], $model);
|
||||
$kirby = $model->kirby();
|
||||
|
||||
if (is_a($model, 'Kirby\Cms\Page') === true) {
|
||||
if ($model instanceof Page) {
|
||||
$parent = $model;
|
||||
$site = $model->site();
|
||||
} else {
|
||||
|
@ -198,41 +204,67 @@ class Pages extends Collection
|
|||
}
|
||||
|
||||
/**
|
||||
* Finds a page in the collection by id.
|
||||
* This works recursively for children and
|
||||
* children of children, etc.
|
||||
* @deprecated 3.7.0 Use `$pages->get()` or `$pages->find()` instead
|
||||
* @todo 3.8.0 Remove method
|
||||
* @codeCoverageIgnore
|
||||
* Finds a page by its ID or URI
|
||||
* @internal Use `$pages->find()` instead
|
||||
*
|
||||
* @param string|null $id
|
||||
* @return mixed
|
||||
* @param string|null $key
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function findById(string $id = null)
|
||||
public function findByKey(string|null $key = null)
|
||||
{
|
||||
Helpers::deprecated('Cms\Pages::findById() has been deprecated and will be removed in Kirby 3.8.0. Use $pages->get() or $pages->find() instead.');
|
||||
if ($key === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findByKey($id);
|
||||
if ($page = $this->findByUuid($key, 'page')) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
// remove trailing or leading slashes
|
||||
$key = trim($key, '/');
|
||||
|
||||
// strip extensions from the id
|
||||
if (strpos($key, '.') !== false) {
|
||||
$info = pathinfo($key);
|
||||
|
||||
if ($info['dirname'] !== '.') {
|
||||
$key = $info['dirname'] . '/' . $info['filename'];
|
||||
} else {
|
||||
$key = $info['filename'];
|
||||
}
|
||||
}
|
||||
|
||||
// try the obvious way
|
||||
if ($page = $this->get($key)) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
// try to find the page by its (translated) URI by stepping through the page tree
|
||||
$start = $this->parent instanceof Page ? $this->parent->id() : '';
|
||||
if ($page = $this->findByKeyRecursive($key, $start, App::instance()->multilang())) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
// for secondary languages, try the full translated URI
|
||||
// (for collections without parent that won't have a result above)
|
||||
if (
|
||||
App::instance()->multilang() === true &&
|
||||
App::instance()->language()->isDefault() === false &&
|
||||
$page = $this->findBy('uri', $key)
|
||||
) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a child or child of a child recursively.
|
||||
* @deprecated 3.7.0 Use `$pages->find()` instead
|
||||
* @todo 3.8.0 Integrate code into `findByKey()` and remove this method
|
||||
* Finds a child or child of a child recursively
|
||||
*
|
||||
* @param string $id
|
||||
* @param string|null $startAt
|
||||
* @param bool $multiLang
|
||||
* @return mixed
|
||||
*/
|
||||
public function findByIdRecursive(string $id, string $startAt = null, bool $multiLang = false, bool $silenceWarning = false)
|
||||
protected function findByKeyRecursive(string $id, string $startAt = null, bool $multiLang = false)
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($silenceWarning !== true) {
|
||||
Helpers::deprecated('Cms\Pages::findByIdRecursive() has been deprecated and will be removed in Kirby 3.8.0. Use $pages->find() instead.');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
$path = explode('/', $id);
|
||||
$item = null;
|
||||
$query = $startAt;
|
||||
|
@ -260,73 +292,6 @@ class Pages extends Collection
|
|||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a page by its ID or URI
|
||||
* @internal Use `$pages->find()` instead
|
||||
*
|
||||
* @param string|null $key
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function findByKey(?string $key = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// remove trailing or leading slashes
|
||||
$key = trim($key, '/');
|
||||
|
||||
// strip extensions from the id
|
||||
if (strpos($key, '.') !== false) {
|
||||
$info = pathinfo($key);
|
||||
|
||||
if ($info['dirname'] !== '.') {
|
||||
$key = $info['dirname'] . '/' . $info['filename'];
|
||||
} else {
|
||||
$key = $info['filename'];
|
||||
}
|
||||
}
|
||||
|
||||
// try the obvious way
|
||||
if ($page = $this->get($key)) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
// try to find the page by its (translated) URI by stepping through the page tree
|
||||
$start = is_a($this->parent, 'Kirby\Cms\Page') === true ? $this->parent->id() : '';
|
||||
if ($page = $this->findByIdRecursive($key, $start, App::instance()->multilang(), true)) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
// for secondary languages, try the full translated URI
|
||||
// (for collections without parent that won't have a result above)
|
||||
if (
|
||||
App::instance()->multilang() === true &&
|
||||
App::instance()->language()->isDefault() === false &&
|
||||
$page = $this->findBy('uri', $key)
|
||||
) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for `$pages->find()`
|
||||
* @deprecated 3.7.0 Use `$pages->find()` instead
|
||||
* @todo 3.8.0 Remove method
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @param string $id
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function findByUri(string $id)
|
||||
{
|
||||
Helpers::deprecated('Cms\Pages::findByUri() has been deprecated and will be removed in Kirby 3.8.0. Use $pages->find() instead.');
|
||||
|
||||
return $this->findByKey($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the currently open page
|
||||
*
|
||||
|
@ -380,7 +345,7 @@ class Pages extends Collection
|
|||
// get object property by cache mode
|
||||
$index = $drafts === true ? $this->indexWithDrafts : $this->index;
|
||||
|
||||
if (is_a($index, 'Kirby\Cms\Pages') === true) {
|
||||
if ($index instanceof self) {
|
||||
return $index;
|
||||
}
|
||||
|
||||
|
@ -451,14 +416,14 @@ class Pages extends Collection
|
|||
}
|
||||
|
||||
// merge an entire collection
|
||||
if (is_a($args[0], self::class) === true) {
|
||||
if ($args[0] instanceof self) {
|
||||
$collection = clone $this;
|
||||
$collection->data = array_merge($collection->data, $args[0]->data);
|
||||
return $collection;
|
||||
}
|
||||
|
||||
// append a single page
|
||||
if (is_a($args[0], 'Kirby\Cms\Page') === true) {
|
||||
if ($args[0] instanceof Page) {
|
||||
$collection = clone $this;
|
||||
return $collection->set($args[0]->id(), $args[0]);
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ class Pagination extends BasePagination
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function firstPageUrl(): ?string
|
||||
public function firstPageUrl(): string|null
|
||||
{
|
||||
return $this->pageUrl(1);
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ class Pagination extends BasePagination
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function lastPageUrl(): ?string
|
||||
public function lastPageUrl(): string|null
|
||||
{
|
||||
return $this->pageUrl($this->lastPage());
|
||||
}
|
||||
|
@ -119,7 +119,7 @@ class Pagination extends BasePagination
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function nextPageUrl(): ?string
|
||||
public function nextPageUrl(): string|null
|
||||
{
|
||||
if ($page = $this->nextPage()) {
|
||||
return $this->pageUrl($page);
|
||||
|
@ -136,7 +136,7 @@ class Pagination extends BasePagination
|
|||
* @param int|null $page
|
||||
* @return string|null
|
||||
*/
|
||||
public function pageUrl(int $page = null): ?string
|
||||
public function pageUrl(int $page = null): string|null
|
||||
{
|
||||
if ($page === null) {
|
||||
return $this->pageUrl($this->page());
|
||||
|
@ -168,7 +168,7 @@ class Pagination extends BasePagination
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function prevPageUrl(): ?string
|
||||
public function prevPageUrl(): string|null
|
||||
{
|
||||
if ($page = $this->prevPage()) {
|
||||
return $this->pageUrl($page);
|
||||
|
|
|
@ -159,12 +159,6 @@ class Permissions
|
|||
*/
|
||||
protected function setAction(string $category, string $action, $setting)
|
||||
{
|
||||
// deprecated fallback for the settings/system view
|
||||
// TODO: remove in 3.8.0
|
||||
if ($category === 'access' && $action === 'settings') {
|
||||
$action = 'system';
|
||||
}
|
||||
|
||||
// wildcard to overwrite the entire category
|
||||
if ($action === '*') {
|
||||
return $this->setCategory($category, $setting);
|
||||
|
|
|
@ -2,10 +2,15 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
use Exception;
|
||||
use Kirby\Cms\System\UpdateStatus;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Represents a Plugin and handles parsing of
|
||||
|
@ -20,15 +25,16 @@ use Kirby\Toolkit\V;
|
|||
*/
|
||||
class Plugin extends Model
|
||||
{
|
||||
protected $extends;
|
||||
protected $info;
|
||||
protected $name;
|
||||
protected $root;
|
||||
protected array $extends;
|
||||
protected string $name;
|
||||
protected string $root;
|
||||
|
||||
// caches
|
||||
protected array|null $info = null;
|
||||
protected UpdateStatus|null $updateStatus = null;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param array|null $arguments
|
||||
* @return mixed|null
|
||||
* Allows access to any composer.json field by method call
|
||||
*/
|
||||
public function __call(string $key, array $arguments = null)
|
||||
{
|
||||
|
@ -36,10 +42,8 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* Plugin constructor
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $extends
|
||||
* @param string $name Plugin name within Kirby (`vendor/plugin`)
|
||||
* @param array $extends Associative array of plugin extensions
|
||||
*/
|
||||
public function __construct(string $name, array $extends = [])
|
||||
{
|
||||
|
@ -53,9 +57,7 @@ class Plugin extends Model
|
|||
|
||||
/**
|
||||
* Returns the array with author information
|
||||
* from the composer file
|
||||
*
|
||||
* @return array
|
||||
* from the composer.json file
|
||||
*/
|
||||
public function authors(): array
|
||||
{
|
||||
|
@ -64,8 +66,6 @@ class Plugin extends Model
|
|||
|
||||
/**
|
||||
* Returns a comma-separated list with all author names
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function authorsNames(): string
|
||||
{
|
||||
|
@ -79,7 +79,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* Returns the associative array of extensions the plugin bundles
|
||||
*/
|
||||
public function extends(): array
|
||||
{
|
||||
|
@ -87,9 +87,8 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the unique id for the plugin
|
||||
*
|
||||
* @return string
|
||||
* Returns the unique ID for the plugin
|
||||
* (alias for the plugin name)
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
|
@ -97,7 +96,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* Returns the raw data from composer.json
|
||||
*/
|
||||
public function info(): array
|
||||
{
|
||||
|
@ -107,7 +106,7 @@ class Plugin extends Model
|
|||
|
||||
try {
|
||||
$info = Data::read($this->manifest());
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
// there is no manifest file or it is invalid
|
||||
$info = [];
|
||||
}
|
||||
|
@ -117,10 +116,8 @@ class Plugin extends Model
|
|||
|
||||
/**
|
||||
* Returns the link to the plugin homepage
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function link(): ?string
|
||||
public function link(): string|null
|
||||
{
|
||||
$info = $this->info();
|
||||
$homepage = $info['homepage'] ?? null;
|
||||
|
@ -133,7 +130,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* Returns the path to the plugin's composer.json
|
||||
*/
|
||||
public function manifest(): string
|
||||
{
|
||||
|
@ -141,7 +138,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* Returns the root where plugin assets are copied to
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
|
@ -149,7 +146,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* Returns the base URL for plugin assets
|
||||
*/
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
|
@ -157,7 +154,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* Returns the plugin name (`vendor/plugin`)
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
|
@ -165,8 +162,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
* Returns a Kirby option value for this plugin
|
||||
*/
|
||||
public function option(string $key)
|
||||
{
|
||||
|
@ -174,7 +170,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* Returns the option prefix (`vendor.plugin`)
|
||||
*/
|
||||
public function prefix(): string
|
||||
{
|
||||
|
@ -182,7 +178,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* Returns the root where the plugin files are stored
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
|
@ -190,11 +186,13 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* Validates and sets the plugin name
|
||||
*
|
||||
* @return $this
|
||||
* @throws \Kirby\Exception\InvalidArgumentException
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the plugin name has an invalid format
|
||||
*/
|
||||
protected function setName(string $name)
|
||||
protected function setName(string $name): static
|
||||
{
|
||||
if (preg_match('!^[a-z0-9-]+\/[a-z0-9-]+$!i', $name) !== 1) {
|
||||
throw new InvalidArgumentException('The plugin name must follow the format "a-z0-9-/a-z0-9-"');
|
||||
|
@ -205,7 +203,7 @@ class Plugin extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* Returns all available plugin metadata
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
|
@ -219,4 +217,88 @@ class Plugin extends Model
|
|||
'version' => $this->version()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the update status object unless the
|
||||
* update check has been disabled for the plugin
|
||||
* @since 3.8.0
|
||||
*
|
||||
* @param array|null $data Custom override for the getkirby.com update data
|
||||
*/
|
||||
public function updateStatus(array|null $data = null): UpdateStatus|null
|
||||
{
|
||||
if ($this->updateStatus !== null) {
|
||||
return $this->updateStatus;
|
||||
}
|
||||
|
||||
$kirby = $this->kirby();
|
||||
$option = $kirby->option('updates.plugins');
|
||||
|
||||
// specific configuration per plugin
|
||||
if (is_array($option) === true) {
|
||||
// filter all option values by glob match
|
||||
$option = A::filter(
|
||||
$option,
|
||||
fn ($value, $key) => fnmatch($key, $this->name()) === true
|
||||
);
|
||||
|
||||
// sort the matches by key length (with longest key first)
|
||||
$keys = array_map('strlen', array_keys($option));
|
||||
array_multisort($keys, SORT_DESC, $option);
|
||||
|
||||
if (count($option) > 0) {
|
||||
// use the first and therefore longest key (= most specific match)
|
||||
$option = reset($option);
|
||||
} else {
|
||||
// fallback to the default option value
|
||||
$option = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($option === null) {
|
||||
$option = $kirby->option('updates') ?? true;
|
||||
}
|
||||
|
||||
if ($option !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->updateStatus = new UpdateStatus($this, false, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized version number
|
||||
* from the composer.json file
|
||||
*/
|
||||
public function version(): string|null
|
||||
{
|
||||
$composerName = $this->info()['name'] ?? null;
|
||||
$version = $this->info()['version'] ?? null;
|
||||
|
||||
try {
|
||||
// if plugin doesn't have version key in composer.json file
|
||||
// try to get version from "vendor/composer/installed.php"
|
||||
$version ??= InstalledVersions::getPrettyVersion($composerName);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
is_string($version) !== true ||
|
||||
$version === '' ||
|
||||
Str::endsWith($version, '+no-version-set')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// normalize the version number to be without leading `v`
|
||||
$version = ltrim($version, 'vV');
|
||||
|
||||
// ensure that the version number now starts with a digit
|
||||
if (preg_match('/^[0-9]/', $version) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $version;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ class Responder
|
|||
* @param bool|null $cache
|
||||
* @return bool|$this
|
||||
*/
|
||||
public function cache(?bool $cache = null)
|
||||
public function cache(bool|null $cache = null)
|
||||
{
|
||||
if ($cache === null) {
|
||||
// never ever cache private responses
|
||||
|
@ -137,7 +137,7 @@ class Responder
|
|||
* @param bool|null $usesAuth
|
||||
* @return bool|$this
|
||||
*/
|
||||
public function usesAuth(?bool $usesAuth = null)
|
||||
public function usesAuth(bool|null $usesAuth = null)
|
||||
{
|
||||
if ($usesAuth === null) {
|
||||
return $this->usesAuth;
|
||||
|
@ -171,7 +171,7 @@ class Responder
|
|||
* @param array|null $usesCookies
|
||||
* @return array|$this
|
||||
*/
|
||||
public function usesCookies(?array $usesCookies = null)
|
||||
public function usesCookies(array|null $usesCookies = null)
|
||||
{
|
||||
if ($usesCookies === null) {
|
||||
return $this->usesCookies;
|
||||
|
@ -352,7 +352,7 @@ class Responder
|
|||
* @param int|null $code
|
||||
* @return $this
|
||||
*/
|
||||
public function redirect(?string $location = null, ?int $code = null)
|
||||
public function redirect(string|null $location = null, int|null $code = null)
|
||||
{
|
||||
$location = Url::to($location ?? '/');
|
||||
$location = Url::unIdn($location);
|
||||
|
|
|
@ -18,12 +18,8 @@ class Response extends \Kirby\Http\Response
|
|||
* Adjusted redirect creation which
|
||||
* parses locations with the Url::to method
|
||||
* first.
|
||||
*
|
||||
* @param string $location
|
||||
* @param int $code
|
||||
* @return static
|
||||
*/
|
||||
public static function redirect(string $location = '/', int $code = 302)
|
||||
public static function redirect(string $location = '/', int $code = 302): static
|
||||
{
|
||||
return parent::redirect(Url::to($location), $code);
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class Role extends Model
|
|||
{
|
||||
try {
|
||||
return static::load('admin');
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
return static::factory(static::defaults()['admin'], $inject);
|
||||
}
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ class Role extends Model
|
|||
{
|
||||
try {
|
||||
return static::load('nobody');
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
return static::factory(static::defaults()['nobody'], $inject);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ class Section extends Component
|
|||
throw new InvalidArgumentException('Undefined section model');
|
||||
}
|
||||
|
||||
if (is_a($attrs['model'], 'Kirby\Cms\Model') === false) {
|
||||
if ($attrs['model'] instanceof Model === false) {
|
||||
throw new InvalidArgumentException('Invalid section model');
|
||||
}
|
||||
|
||||
|
|
|
@ -164,9 +164,9 @@ class Site extends ModelWithContent
|
|||
{
|
||||
if ($relative === true) {
|
||||
return 'site';
|
||||
} else {
|
||||
return $this->kirby()->url('api') . '/site';
|
||||
}
|
||||
|
||||
return $this->kirby()->url('api') . '/site';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,7 +176,7 @@ class Site extends ModelWithContent
|
|||
*/
|
||||
public function blueprint()
|
||||
{
|
||||
if (is_a($this->blueprint, 'Kirby\Cms\SiteBlueprint') === true) {
|
||||
if ($this->blueprint instanceof SiteBlueprint) {
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
|
@ -210,7 +210,7 @@ class Site extends ModelWithContent
|
|||
* @param string|null $languageCode
|
||||
* @return array
|
||||
*/
|
||||
public function contentFileData(array $data, ?string $languageCode = null): array
|
||||
public function contentFileData(array $data, string|null $languageCode = null): array
|
||||
{
|
||||
return A::prepend($data, [
|
||||
'title' => $data['title'] ?? null,
|
||||
|
@ -235,7 +235,7 @@ class Site extends ModelWithContent
|
|||
*/
|
||||
public function errorPage()
|
||||
{
|
||||
if (is_a($this->errorPage, 'Kirby\Cms\Page') === true) {
|
||||
if ($this->errorPage instanceof Page) {
|
||||
return $this->errorPage;
|
||||
}
|
||||
|
||||
|
@ -274,7 +274,7 @@ class Site extends ModelWithContent
|
|||
*/
|
||||
public function homePage()
|
||||
{
|
||||
if (is_a($this->homePage, 'Kirby\Cms\Page') === true) {
|
||||
if ($this->homePage instanceof Page) {
|
||||
return $this->homePage;
|
||||
}
|
||||
|
||||
|
@ -327,7 +327,7 @@ class Site extends ModelWithContent
|
|||
*/
|
||||
public function is($site): bool
|
||||
{
|
||||
if (is_a($site, 'Kirby\Cms\Site') === false) {
|
||||
if ($site instanceof self === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -364,7 +364,7 @@ class Site extends ModelWithContent
|
|||
* @param string|null $handler
|
||||
* @return int|string
|
||||
*/
|
||||
public function modified(?string $format = null, ?string $handler = null)
|
||||
public function modified(string|null $format = null, string|null $handler = null)
|
||||
{
|
||||
return Dir::modified(
|
||||
$this->root(),
|
||||
|
@ -386,19 +386,19 @@ class Site extends ModelWithContent
|
|||
* otherwise e.g. `notes/across-the-ocean`
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function page(?string $path = null)
|
||||
public function page(string|null $path = null)
|
||||
{
|
||||
if ($path !== null) {
|
||||
return $this->find($path);
|
||||
}
|
||||
|
||||
if (is_a($this->page, 'Kirby\Cms\Page') === true) {
|
||||
if ($this->page instanceof Page) {
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->page = $this->homePage();
|
||||
} catch (LogicException $e) {
|
||||
} catch (LogicException) {
|
||||
return $this->page = null;
|
||||
}
|
||||
}
|
||||
|
@ -439,7 +439,7 @@ class Site extends ModelWithContent
|
|||
* @internal
|
||||
* @return string|null
|
||||
*/
|
||||
public function previewUrl(): ?string
|
||||
public function previewUrl(): string|null
|
||||
{
|
||||
$preview = $this->blueprint()->preview();
|
||||
|
||||
|
@ -485,7 +485,7 @@ class Site extends ModelWithContent
|
|||
* @param array $params
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function search(?string $query = null, $params = [])
|
||||
public function search(string|null $query = null, $params = [])
|
||||
{
|
||||
return $this->index()->search($query, $params);
|
||||
}
|
||||
|
@ -496,7 +496,7 @@ class Site extends ModelWithContent
|
|||
* @param array|null $blueprint
|
||||
* @return $this
|
||||
*/
|
||||
protected function setBlueprint(?array $blueprint = null)
|
||||
protected function setBlueprint(array|null $blueprint = null)
|
||||
{
|
||||
if ($blueprint !== null) {
|
||||
$blueprint['model'] = $this;
|
||||
|
@ -555,7 +555,7 @@ class Site extends ModelWithContent
|
|||
* @param string|null $url
|
||||
* @return $this
|
||||
*/
|
||||
protected function setUrl(?string $url = null)
|
||||
protected function setUrl(string|null $url = null)
|
||||
{
|
||||
$this->url = $url;
|
||||
return $this;
|
||||
|
@ -587,7 +587,7 @@ class Site extends ModelWithContent
|
|||
* @param string|null $language
|
||||
* @return string
|
||||
*/
|
||||
public function url(?string $language = null): string
|
||||
public function url(string|null $language = null): string
|
||||
{
|
||||
if ($language !== null || $this->kirby()->multilang() === true) {
|
||||
return $this->urlForLanguage($language);
|
||||
|
@ -604,7 +604,7 @@ class Site extends ModelWithContent
|
|||
* @param array|null $options
|
||||
* @return string
|
||||
*/
|
||||
public function urlForLanguage(?string $languageCode = null, ?array $options = null): string
|
||||
public function urlForLanguage(string|null $languageCode = null, array|null $options = null): string
|
||||
{
|
||||
if ($language = $this->kirby()->language($languageCode)) {
|
||||
return $language->url();
|
||||
|
@ -623,7 +623,7 @@ class Site extends ModelWithContent
|
|||
* @param string|null $languageCode
|
||||
* @return \Kirby\Cms\Page
|
||||
*/
|
||||
public function visit($page, ?string $languageCode = null)
|
||||
public function visit($page, string|null $languageCode = null)
|
||||
{
|
||||
if ($languageCode !== null) {
|
||||
$this->kirby()->setCurrentTranslation($languageCode);
|
||||
|
@ -636,7 +636,7 @@ class Site extends ModelWithContent
|
|||
}
|
||||
|
||||
// handle invalid pages
|
||||
if (is_a($page, 'Kirby\Cms\Page') === false) {
|
||||
if ($page instanceof Page === false) {
|
||||
throw new InvalidArgumentException('Invalid page object');
|
||||
}
|
||||
|
||||
|
@ -659,41 +659,4 @@ class Site extends ModelWithContent
|
|||
{
|
||||
return Dir::wasModifiedAfter($this->root(), $time);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deprecated!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
Helpers::deprecated('Cms\Site::panelPath() has been deprecated and will be removed in Kirby 3.8.0. Use $site->panel()->path() instead.');
|
||||
return $this->panel()->path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @todo Remove in 3.8.0
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function panelUrl(bool $relative = false): string
|
||||
{
|
||||
Helpers::deprecated('Cms\Site::panelUrl() has been deprecated and will be removed in Kirby 3.8.0. Use $site->panel()->url() instead.');
|
||||
return $this->panel()->url($relative);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ class Structure extends Collection
|
|||
*/
|
||||
public function __set(string $id, $props): void
|
||||
{
|
||||
if (is_a($props, 'Kirby\Cms\StructureObject') === true) {
|
||||
if ($props instanceof StructureObject) {
|
||||
$object = $props;
|
||||
} else {
|
||||
if (is_array($props) === false) {
|
||||
|
|
|
@ -81,7 +81,7 @@ class StructureObject extends Model
|
|||
*/
|
||||
public function content()
|
||||
{
|
||||
if (is_a($this->content, 'Kirby\Cms\Content') === true) {
|
||||
if ($this->content instanceof Content) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ class StructureObject extends Model
|
|||
*/
|
||||
public function is($structure): bool
|
||||
{
|
||||
if (is_a($structure, 'Kirby\Cms\StructureObject') === false) {
|
||||
if ($structure instanceof self === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Cms\System\UpdateStatus;
|
||||
use Kirby\Data\Json;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
@ -35,6 +36,9 @@ class System
|
|||
*/
|
||||
protected $app;
|
||||
|
||||
// cache
|
||||
protected UpdateStatus|null $updateStatus = null;
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\App $app
|
||||
*/
|
||||
|
@ -94,7 +98,7 @@ class System
|
|||
* @param string $folder 'git', 'content', 'site', 'kirby'
|
||||
* @return string|null
|
||||
*/
|
||||
public function exposedFileUrl(string $folder): ?string
|
||||
public function exposedFileUrl(string $folder): string|null
|
||||
{
|
||||
if (!$url = $this->folderUrl($folder)) {
|
||||
return null;
|
||||
|
@ -140,7 +144,7 @@ class System
|
|||
* @param string $folder 'git', 'content', 'site', 'kirby'
|
||||
* @return string|null
|
||||
*/
|
||||
public function folderUrl(string $folder): ?string
|
||||
public function folderUrl(string $folder): string|null
|
||||
{
|
||||
$index = $this->app->root('index');
|
||||
|
||||
|
@ -196,28 +200,28 @@ class System
|
|||
// init /site/accounts
|
||||
try {
|
||||
Dir::make($this->app->root('accounts'));
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
throw new PermissionException('The accounts directory could not be created');
|
||||
}
|
||||
|
||||
// init /site/sessions
|
||||
try {
|
||||
Dir::make($this->app->root('sessions'));
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
throw new PermissionException('The sessions directory could not be created');
|
||||
}
|
||||
|
||||
// init /content
|
||||
try {
|
||||
Dir::make($this->app->root('content'));
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
throw new PermissionException('The content directory could not be created');
|
||||
}
|
||||
|
||||
// init /media
|
||||
try {
|
||||
Dir::make($this->app->root('media'));
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
throw new PermissionException('The media directory could not be created');
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +281,7 @@ class System
|
|||
{
|
||||
try {
|
||||
$license = Json::read($this->app->root('license'));
|
||||
} catch (Throwable $e) {
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -318,12 +322,11 @@ class System
|
|||
|
||||
// only return the actual license key if the
|
||||
// current user has appropriate permissions
|
||||
$user = $this->app->user();
|
||||
if ($user && $user->isAdmin() === true) {
|
||||
if ($this->app->user()?->isAdmin() === true) {
|
||||
return $license['license'];
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -454,7 +457,7 @@ class System
|
|||
public function php(): bool
|
||||
{
|
||||
return
|
||||
version_compare(PHP_VERSION, '7.4.0', '>=') === true &&
|
||||
version_compare(PHP_VERSION, '8.0.0', '>=') === true &&
|
||||
version_compare(PHP_VERSION, '8.2.0', '<') === true;
|
||||
}
|
||||
|
||||
|
@ -544,23 +547,19 @@ class System
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function serverSoftware(): ?string
|
||||
public function serverSoftware(): string|null
|
||||
{
|
||||
if ($servers = $this->app->option('servers')) {
|
||||
$servers = A::wrap($servers);
|
||||
} else {
|
||||
$servers = [
|
||||
'apache',
|
||||
'caddy',
|
||||
'litespeed',
|
||||
'nginx',
|
||||
'php'
|
||||
];
|
||||
}
|
||||
$servers = $this->app->option('servers', [
|
||||
'apache',
|
||||
'caddy',
|
||||
'litespeed',
|
||||
'nginx',
|
||||
'php'
|
||||
]);
|
||||
|
||||
$software = $this->app->environment()->get('SERVER_SOFTWARE', '');
|
||||
|
||||
preg_match('!(' . implode('|', $servers) . ')!i', $software, $matches);
|
||||
preg_match('!(' . implode('|', A::wrap($servers)) . ')!i', $software, $matches);
|
||||
|
||||
return $matches[0] ?? null;
|
||||
}
|
||||
|
@ -620,6 +619,33 @@ class System
|
|||
return $this->status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the update status object unless
|
||||
* the update check for Kirby has been disabled
|
||||
* @since 3.8.0
|
||||
*
|
||||
* @param array|null $data Custom override for the getkirby.com update data
|
||||
*/
|
||||
public function updateStatus(array|null $data = null): UpdateStatus|null
|
||||
{
|
||||
if ($this->updateStatus !== null) {
|
||||
return $this->updateStatus;
|
||||
}
|
||||
|
||||
$kirby = $this->app;
|
||||
$option = $kirby->option('updates.kirby') ?? $kirby->option('updates') ?? true;
|
||||
|
||||
if ($option === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->updateStatus = new UpdateStatus(
|
||||
$kirby,
|
||||
$option === 'security',
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade to the new folder separator
|
||||
*
|
||||
|
|
771
kirby/src/Cms/System/UpdateStatus.php
Normal file
771
kirby/src/Cms/System/UpdateStatus.php
Normal file
|
@ -0,0 +1,771 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms\System;
|
||||
|
||||
use Composer\Semver\Semver;
|
||||
use Exception;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Plugin;
|
||||
use Kirby\Exception\Exception as KirbyException;
|
||||
use Kirby\Http\Remote;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Checks for updates and affected vulnerabilities
|
||||
* @since 3.8.0
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class UpdateStatus
|
||||
{
|
||||
/**
|
||||
* Host to request the update data from
|
||||
*/
|
||||
public static string $host = 'https://assets.getkirby.com';
|
||||
|
||||
/**
|
||||
* Marker that stores whether a previous remote
|
||||
* request timed out
|
||||
*/
|
||||
protected static bool $timedOut = false;
|
||||
|
||||
// props set in constructor
|
||||
protected App $app;
|
||||
protected string|null $currentVersion;
|
||||
protected array|null $data;
|
||||
protected string|null $pluginName;
|
||||
protected bool $securityOnly;
|
||||
|
||||
// props updated throughout the class
|
||||
protected array $exceptions = [];
|
||||
protected bool|null $noVulns = null;
|
||||
|
||||
// caches
|
||||
protected array $messages;
|
||||
protected array $targetData;
|
||||
protected array|bool $versionEntry;
|
||||
protected array $vulnerabilities;
|
||||
|
||||
/**
|
||||
* @param array|null $data Custom override for the getkirby.com update data
|
||||
*/
|
||||
public function __construct(
|
||||
App|Plugin $package,
|
||||
bool $securityOnly = false,
|
||||
array|null $data = null
|
||||
) {
|
||||
if ($package instanceof App) {
|
||||
$this->app = $package;
|
||||
$this->pluginName = null;
|
||||
} else {
|
||||
$this->app = $package->kirby();
|
||||
$this->pluginName = $package->name();
|
||||
}
|
||||
|
||||
$this->securityOnly = $securityOnly;
|
||||
$this->currentVersion = $package->version();
|
||||
|
||||
$this->data = $data ?? $this->loadData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently installed version
|
||||
*/
|
||||
public function currentVersion(): string|null
|
||||
{
|
||||
return $this->currentVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of exception objects that were
|
||||
* collected during data fetching and processing
|
||||
*/
|
||||
public function exceptions(): array
|
||||
{
|
||||
return $this->exceptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of exception message strings that
|
||||
* were collected during data fetching and processing
|
||||
*/
|
||||
public function exceptionMessages(): array
|
||||
{
|
||||
return array_map(fn ($e) => $e->getMessage(), $this->exceptions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Panel icon for the status value
|
||||
*
|
||||
* @return string 'check'|'alert'|'info'
|
||||
*/
|
||||
public function icon(): string
|
||||
{
|
||||
return match ($this->status()) {
|
||||
'up-to-date', 'not-vulnerable' => 'check',
|
||||
'security-update', 'security-upgrade' => 'alert',
|
||||
'update', 'upgrade' => 'info',
|
||||
default => 'question'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable and translated label
|
||||
* for the update status
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return I18n::template(
|
||||
'system.updateStatus.' . $this->status(),
|
||||
'?',
|
||||
['version' => $this->targetVersion() ?? '?']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest available version
|
||||
*/
|
||||
public function latestVersion(): string|null
|
||||
{
|
||||
return $this->data['latest'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all security messages unless no data
|
||||
* is available
|
||||
*/
|
||||
public function messages(): array|null
|
||||
{
|
||||
if (isset($this->messages) === true) {
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->data === null ||
|
||||
$this->currentVersion === null ||
|
||||
$this->currentVersion === ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = $this->pluginName ? 'plugin' : 'kirby';
|
||||
|
||||
// collect all matching custom messages
|
||||
$filters = [
|
||||
'kirby' => $this->app->version(),
|
||||
'php' => phpversion()
|
||||
];
|
||||
|
||||
if ($type === 'plugin') {
|
||||
$filters['plugin'] = $this->currentVersion;
|
||||
}
|
||||
|
||||
$messages = $this->filterArrayByVersion(
|
||||
$this->data['messages'] ?? [],
|
||||
$filters,
|
||||
'while filtering messages'
|
||||
);
|
||||
|
||||
// add a message for each vulnerability
|
||||
// the current version is affected by
|
||||
foreach ($this->vulnerabilities() as $vulnerability) {
|
||||
if ($type === 'plugin') {
|
||||
$vulnerability['plugin'] = $this->pluginName;
|
||||
}
|
||||
|
||||
$messages[] = [
|
||||
'text' => I18n::template(
|
||||
'system.issues.vulnerability.' . $type,
|
||||
null,
|
||||
$vulnerability
|
||||
),
|
||||
'link' => $vulnerability['link'] ?? null,
|
||||
'icon' => 'bug'
|
||||
];
|
||||
}
|
||||
|
||||
// add special message for end-of-life versions
|
||||
$versionEntry = $this->versionEntry();
|
||||
if (($versionEntry['status'] ?? null) === 'end-of-life') {
|
||||
$messages[] = [
|
||||
'text' => match ($type) {
|
||||
'plugin' => I18n::template(
|
||||
'system.issues.eol.plugin',
|
||||
null,
|
||||
['plugin' => $this->pluginName]
|
||||
),
|
||||
default => I18n::translate('system.issues.eol.kirby')
|
||||
},
|
||||
'link' => $versionEntry['status-link'] ?? 'https://getkirby.com/security/end-of-life',
|
||||
'icon' => 'bell'
|
||||
];
|
||||
}
|
||||
|
||||
return $this->messages = $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw status value
|
||||
*
|
||||
* @return string 'up-to-date'|'not-vulnerable'|'security-update'|
|
||||
* 'security-upgrade'|'update'|'upgrade'|'unreleased'|'error'
|
||||
*/
|
||||
public function status(): string
|
||||
{
|
||||
return $this->targetData()['status'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Version that is suggested for the update/upgrade
|
||||
*/
|
||||
public function targetVersion(): string|null
|
||||
{
|
||||
return $this->targetData()['version'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Panel theme for the status value
|
||||
*
|
||||
* @return string 'positive'|'negative'|'info'|'notice'
|
||||
*/
|
||||
public function theme(): string
|
||||
{
|
||||
return match ($this->status()) {
|
||||
'up-to-date', 'not-vulnerable' => 'positive',
|
||||
'security-update', 'security-upgrade' => 'negative',
|
||||
'update', 'upgrade' => 'info',
|
||||
default => 'notice'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most important human-readable
|
||||
* status information as array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'currentVersion' => $this->currentVersion() ?? '?',
|
||||
'icon' => $this->icon(),
|
||||
'label' => $this->label(),
|
||||
'latestVersion' => $this->latestVersion() ?? '?',
|
||||
'pluginName' => $this->pluginName,
|
||||
'theme' => $this->theme(),
|
||||
'url' => $this->url(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* URL of the target version with fallback
|
||||
* to the URL of the current version;
|
||||
* `null` is returned if no URL is known
|
||||
*/
|
||||
public function url(): string|null
|
||||
{
|
||||
return $this->targetData()['url'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all vulnerabilities the current version
|
||||
* is affected by unless no data is available
|
||||
*/
|
||||
public function vulnerabilities(): array|null
|
||||
{
|
||||
if (isset($this->vulnerabilities) === true) {
|
||||
return $this->vulnerabilities;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->data === null ||
|
||||
$this->currentVersion === null ||
|
||||
$this->currentVersion === ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// shortcut for versions without vulnerabilities
|
||||
$this->versionEntry();
|
||||
if ($this->noVulns === true) {
|
||||
return $this->vulnerabilities = [];
|
||||
}
|
||||
|
||||
// unstable releases are released before their respective
|
||||
// stable release and would not be matched by the constraints,
|
||||
// but they will likely also contain the same vulnerabilities;
|
||||
// so we strip off any non-numeric version modifiers from the end
|
||||
preg_match('/^([0-9.]+)/', $this->currentVersion, $matches);
|
||||
$currentVersion = $matches[1];
|
||||
|
||||
$vulnerabilities = $this->filterArrayByVersion(
|
||||
$this->data['incidents'] ?? [],
|
||||
['affected' => $currentVersion],
|
||||
'while filtering incidents'
|
||||
);
|
||||
|
||||
// sort the vulnerabilities by severity (with critical first)
|
||||
$severities = array_map(
|
||||
fn ($vulnerability) => match ($vulnerability['severity'] ?? null) {
|
||||
'critical' => 4,
|
||||
'high' => 3,
|
||||
'medium' => 2,
|
||||
'low' => 1,
|
||||
default => 0
|
||||
},
|
||||
$vulnerabilities
|
||||
);
|
||||
array_multisort($severities, SORT_DESC, $vulnerabilities);
|
||||
|
||||
return $this->vulnerabilities = $vulnerabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a version against a Composer version constraint
|
||||
* and returns whether the constraint is satisfied
|
||||
*
|
||||
* @param string $reason Suffix for error messages
|
||||
*/
|
||||
protected function checkConstraint(string $version, string $constraint, string $reason): bool
|
||||
{
|
||||
try {
|
||||
return Semver::satisfies($version, $constraint);
|
||||
} catch (Exception $e) {
|
||||
$package = $this->packageName();
|
||||
$message = 'Error comparing version constraint for ' . $package . ' ' . $reason . ': ' . $e->getMessage();
|
||||
|
||||
$exception = new KirbyException([
|
||||
'fallback' => $message,
|
||||
'previous' => $e
|
||||
]);
|
||||
$this->exceptions[] = $exception;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a two-level array with one or multiple version constraints
|
||||
* for each value by one or multiple version filters;
|
||||
* values that don't contain the filter keys are removed
|
||||
*
|
||||
* @param array $array Array that contains associative arrays
|
||||
* @param array $filters Associative array `field => version`
|
||||
* @param string $reason Suffix for error messages
|
||||
*/
|
||||
protected function filterArrayByVersion(array $array, array $filters, string $reason): array
|
||||
{
|
||||
return array_filter($array, function ($item) use ($filters, $reason): bool {
|
||||
foreach ($filters as $key => $version) {
|
||||
if (isset($item[$key]) !== true) {
|
||||
$package = $this->packageName();
|
||||
$this->exceptions[] = new KirbyException('Missing constraint ' . $key . ' for ' . $package . ' ' . $reason);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->checkConstraint($version, $item[$key], $reason) !== true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the minimum possible security update
|
||||
* to fix all known vulnerabilities
|
||||
*
|
||||
* @return string|null Version number of the update or
|
||||
* `null` if no free update is possible
|
||||
*/
|
||||
protected function findMinimumSecurityUpdate(): string|null
|
||||
{
|
||||
$versionEntry = $this->versionEntry();
|
||||
if ($versionEntry === null || isset($versionEntry['latest']) !== true) {
|
||||
return null; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$affected = $this->vulnerabilities();
|
||||
$incidents = $this->data['incidents'] ?? [];
|
||||
$maxVersion = $versionEntry['latest'];
|
||||
|
||||
// increase the target version number until there are no vulnerabilities
|
||||
$version = $this->currentVersion;
|
||||
$iterations = 0;
|
||||
while (empty($affected) === false) {
|
||||
// protect against infinite loops if the
|
||||
// input data is contradicting itself
|
||||
$iterations++;
|
||||
if ($iterations > 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// if we arrived at the `$maxVersion` but still haven't found
|
||||
// a version without vulnerabilities, we cannot suggest a version
|
||||
if ($version === $maxVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// find the minimum version that fixes all affected vulnerabilities
|
||||
foreach ($affected as $incident) {
|
||||
$incidentVersion = null;
|
||||
foreach (Str::split($incident['fixed'], ',') as $fixed) {
|
||||
// skip versions of other major releases
|
||||
if (
|
||||
version_compare($fixed, $this->currentVersion, '<') === true ||
|
||||
version_compare($fixed, $maxVersion, '>') === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// find the minimum version that fixes this specific vulnerability
|
||||
if (
|
||||
$incidentVersion === null ||
|
||||
version_compare($fixed, $incidentVersion, '<') === true
|
||||
) {
|
||||
$incidentVersion = $fixed;
|
||||
}
|
||||
}
|
||||
|
||||
// verify that we found at least one possible version;
|
||||
// otherwise try the `$maxVersion` as a last chance before
|
||||
// concluding at the top that we cannot solve the task
|
||||
if ($incidentVersion === null) {
|
||||
$incidentVersion = $maxVersion;
|
||||
}
|
||||
|
||||
// we need a version that fixes all vulnerabilities, so use the
|
||||
// "largest of the smallest" fixed versions
|
||||
if (version_compare($incidentVersion, $version, '>') === true) {
|
||||
$version = $incidentVersion;
|
||||
}
|
||||
}
|
||||
|
||||
// run another loop to verify that the suggested version
|
||||
// doesn't have any known vulnerabilities on its own
|
||||
$affected = $this->filterArrayByVersion(
|
||||
$incidents,
|
||||
['affected' => $version],
|
||||
'while filtering incidents'
|
||||
);
|
||||
}
|
||||
|
||||
return $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the getkirby.com update data
|
||||
* from cache or via HTTP
|
||||
*/
|
||||
protected function loadData(): array|null
|
||||
{
|
||||
// try to get the data from cache
|
||||
$cache = $this->app->cache('updates');
|
||||
$key = (
|
||||
$this->pluginName ?
|
||||
'plugins/' . $this->pluginName :
|
||||
'security'
|
||||
);
|
||||
|
||||
// try to return from cache;
|
||||
// invalidate the cache after updates
|
||||
$data = $cache->get($key);
|
||||
if (
|
||||
is_array($data) === true &&
|
||||
$data['_version'] === $this->currentVersion
|
||||
) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// exception message (on previous request error)
|
||||
if (is_string($data) === true) {
|
||||
// restore the exception to make it visible when debugging
|
||||
$this->exceptions[] = new KirbyException($data);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// before we request the data, ensure we have a writable cache;
|
||||
// this reduces strain on the CDN from repeated requests
|
||||
if ($cache->enabled() === false) {
|
||||
$this->exceptions[] = new KirbyException('Cannot check for updates without a working "updates" cache');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// first catch every exception;
|
||||
// we collect it below for debugging
|
||||
try {
|
||||
if (static::$timedOut === true) {
|
||||
throw new Exception('Previous remote request timed out'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$response = Remote::get(
|
||||
static::$host . '/' . $key . '.json',
|
||||
['timeout' => 2]
|
||||
);
|
||||
|
||||
// allow status code HTTP 200 or 0 (e.g. for the file:// protocol)
|
||||
if (in_array($response->code(), [0, 200], true) !== true) {
|
||||
throw new Exception('HTTP error ' . $response->code()); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
if (is_array($data) !== true) {
|
||||
throw new Exception('Invalid JSON data');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$package = $this->packageName();
|
||||
$message = 'Could not load update data for ' . $package . ': ' . $e->getMessage();
|
||||
|
||||
$exception = new KirbyException([
|
||||
'fallback' => $message,
|
||||
'previous' => $e
|
||||
]);
|
||||
$this->exceptions[] = $exception;
|
||||
|
||||
// if the request timed out, prevent additional
|
||||
// requests for other packages (e.g. plugins)
|
||||
// to avoid long Panel hangs
|
||||
if ($e->getCode() === 28) {
|
||||
static::$timedOut = true; // @codeCoverageIgnore
|
||||
} elseif (static::$timedOut === false) {
|
||||
// different error than timeout;
|
||||
// prevent additional requests in the
|
||||
// next three days (e.g. if a plugin
|
||||
// does not have a page on getkirby.com)
|
||||
// by caching the exception message
|
||||
// instead of the data array
|
||||
$cache->set($key, $exception->getMessage(), 3 * 24 * 60);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// also cache the current version to
|
||||
// invalidate the cache after updates
|
||||
// (ensures that the update status is
|
||||
// fresh directly after the update to
|
||||
// avoid confusion with outdated info)
|
||||
$data['_version'] = $this->currentVersion;
|
||||
|
||||
// cache the retrieved data for three days
|
||||
$cache->set($key, $data, 3 * 24 * 60);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable package name for error messages
|
||||
*/
|
||||
protected function packageName(): string
|
||||
{
|
||||
return $this->pluginName ? 'plugin ' . $this->pluginName : 'Kirby';
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the update check and returns data for the
|
||||
* target version (with fallback and error handling)
|
||||
*/
|
||||
protected function targetData(): array
|
||||
{
|
||||
if (isset($this->targetData) === true) {
|
||||
return $this->targetData;
|
||||
}
|
||||
|
||||
// check if we have valid data to compare to
|
||||
$versionEntry = $this->versionEntry();
|
||||
if ($versionEntry === null) {
|
||||
$version = $this->currentVersion ?? $this->data['latest'] ?? null;
|
||||
|
||||
return $this->targetData = [
|
||||
'status' => 'error',
|
||||
'url' => $version ? $this->urlFor($version, 'changes') : null,
|
||||
'version' => null
|
||||
];
|
||||
}
|
||||
|
||||
// check if the current version is the latest available
|
||||
if (($versionEntry['status'] ?? null) === 'latest') {
|
||||
return $this->targetData = [
|
||||
'status' => 'up-to-date',
|
||||
'url' => $this->urlFor($this->currentVersion, 'changes'),
|
||||
'version' => null
|
||||
];
|
||||
}
|
||||
|
||||
// check if the current version is unreleased
|
||||
if (($versionEntry['status'] ?? null) === 'unreleased') {
|
||||
return $this->targetData = [
|
||||
'status' => 'unreleased',
|
||||
'url' => null,
|
||||
'version' => null
|
||||
];
|
||||
}
|
||||
|
||||
// check if the installation is vulnerable;
|
||||
// minimum possible security fixes are preferred
|
||||
// over all other updates and upgrades
|
||||
if (count($this->vulnerabilities()) > 0) {
|
||||
$update = $this->findMinimumSecurityUpdate();
|
||||
|
||||
if ($update !== null) {
|
||||
// a free security update was found
|
||||
return $this->targetData = [
|
||||
'status' => 'security-update',
|
||||
'url' => $this->urlFor($update, 'changes'),
|
||||
'version' => $update
|
||||
];
|
||||
}
|
||||
|
||||
// only a paid security upgrade is possible
|
||||
return $this->targetData = [
|
||||
'status' => 'security-upgrade',
|
||||
'url' => $this->urlFor($this->currentVersion, 'upgrade'),
|
||||
'version' => $this->data['latest'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
// check if the user limited update checking to security updates
|
||||
if ($this->securityOnly === true) {
|
||||
return $this->targetData = [
|
||||
'status' => 'not-vulnerable',
|
||||
'url' => $this->urlFor($this->currentVersion, 'changes'),
|
||||
'version' => null
|
||||
];
|
||||
}
|
||||
|
||||
// check if free updates are possible from the current version
|
||||
$latest = $versionEntry['latest'] ?? null;
|
||||
if (is_string($latest) === true && $latest !== $this->currentVersion) {
|
||||
return $this->targetData = [
|
||||
'status' => 'update',
|
||||
'url' => $this->urlFor($latest, 'changes'),
|
||||
'version' => $latest
|
||||
];
|
||||
}
|
||||
|
||||
// no free update is possible, but we are not on the latest version,
|
||||
// so the overall latest version must be an upgrade
|
||||
return $this->targetData = [
|
||||
'status' => 'upgrade',
|
||||
'url' => $this->urlFor($this->currentVersion, 'upgrade'),
|
||||
'version' => $this->data['latest'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL for a specific version and purpose
|
||||
*/
|
||||
protected function urlFor(string $version, string $purpose): string|null
|
||||
{
|
||||
if ($this->data === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// find the first matching entry
|
||||
$url = null;
|
||||
foreach ($this->data['urls'] ?? [] as $constraint => $entry) {
|
||||
// filter out every entry that does not match the version
|
||||
if ($this->checkConstraint($version, $constraint, 'while finding URL') !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// we found a result
|
||||
$url = $entry[$purpose] ?? null;
|
||||
if ($url !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($url === null) {
|
||||
$package = $this->packageName();
|
||||
$message = 'No matching URL found for ' . $package . '@' . $version;
|
||||
|
||||
$this->exceptions[] = new KirbyException($message);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// insert the URL template placeholders
|
||||
return Str::template($url, [
|
||||
'current' => $this->currentVersion,
|
||||
'version' => $version
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the first matching version entry from
|
||||
* the data array unless no data is available
|
||||
*/
|
||||
protected function versionEntry(): array|null
|
||||
{
|
||||
if (isset($this->versionEntry) === true) {
|
||||
// no version entry found on last call
|
||||
if ($this->versionEntry === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->versionEntry;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->data === null ||
|
||||
$this->currentVersion === null ||
|
||||
$this->currentVersion === ''
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// special check for unreleased versions
|
||||
$latest = $this->data['latest'] ?? null;
|
||||
if (
|
||||
$latest !== null &&
|
||||
version_compare($this->currentVersion, $latest, '>') === true
|
||||
) {
|
||||
return [
|
||||
'status' => 'unreleased'
|
||||
];
|
||||
}
|
||||
|
||||
$versionEntry = null;
|
||||
foreach ($this->data['versions'] ?? [] as $constraint => $entry) {
|
||||
// filter out every entry that does not match the current version
|
||||
if ($this->checkConstraint($this->currentVersion, $constraint, 'while finding version entry') !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($entry['status'] ?? null) === 'no-vulnerabilities') {
|
||||
$this->noVulns = true;
|
||||
|
||||
// use the next matching version entry with
|
||||
// more specific update information
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($entry['status'] ?? null) === 'latest') {
|
||||
$this->noVulns = true;
|
||||
}
|
||||
|
||||
// we found a result
|
||||
$versionEntry = $entry;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($versionEntry === null) {
|
||||
$package = $this->packageName();
|
||||
$message = 'No matching version entry found for ' . $package . '@' . $this->currentVersion;
|
||||
|
||||
$this->exceptions[] = new KirbyException($message);
|
||||
}
|
||||
|
||||
$this->versionEntry = $versionEntry ?? false;
|
||||
return $versionEntry;
|
||||
}
|
||||
}
|
|
@ -122,13 +122,13 @@ class Template
|
|||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function file(): ?string
|
||||
public function file(): string|null
|
||||
{
|
||||
if ($this->hasDefaultType() === true) {
|
||||
try {
|
||||
// Try the default template in the default template directory.
|
||||
return F::realpath($this->root() . '/' . $this->name() . '.' . $this->extension(), $this->root());
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
// ignore errors, continue searching
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ class Template
|
|||
try {
|
||||
// Try the template with type extension in the default template directory.
|
||||
return F::realpath($this->root() . '/' . $name . '.' . $this->extension(), $this->root());
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
// Look for the template with type extension provided by an extension.
|
||||
// This might be null if the template does not exist.
|
||||
return App::instance()->extension($this->store(), $name);
|
||||
|
|
|
@ -116,7 +116,7 @@ class Translation
|
|||
* @param string|null $default
|
||||
* @return string|null
|
||||
*/
|
||||
public function get(string $key, string $default = null): ?string
|
||||
public function get(string $key, string $default = null): string|null
|
||||
{
|
||||
return $this->data[$key] ?? $default;
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ class Translation
|
|||
{
|
||||
try {
|
||||
$data = array_merge(Data::read($root), $inject);
|
||||
} catch (Exception $e) {
|
||||
} catch (Exception) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
|
|
|
@ -21,12 +21,10 @@ use Kirby\Http\Url as BaseUrl;
|
|||
*/
|
||||
class Url extends BaseUrl
|
||||
{
|
||||
public static $home = null;
|
||||
public static string|null $home = null;
|
||||
|
||||
/**
|
||||
* Returns the Url to the homepage
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function home(): string
|
||||
{
|
||||
|
@ -34,13 +32,10 @@ class Url extends BaseUrl
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates an absolute Url to a template asset if it exists. This is used in the `css()` and `js()` helpers
|
||||
*
|
||||
* @param string $assetPath
|
||||
* @param string $extension
|
||||
* @return string|null
|
||||
* Creates an absolute Url to a template asset if it exists.
|
||||
* This is used in the `css()` and `js()` helpers
|
||||
*/
|
||||
public static function toTemplateAsset(string $assetPath, string $extension)
|
||||
public static function toTemplateAsset(string $assetPath, string $extension): string|null
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$page = $kirby->site()->page();
|
||||
|
@ -54,11 +49,9 @@ class Url extends BaseUrl
|
|||
/**
|
||||
* Smart resolver for internal and external urls
|
||||
*
|
||||
* @param string|null $path
|
||||
* @param array|string|null $options Either an array of options for the Uri class or a language string
|
||||
* @return string
|
||||
*/
|
||||
public static function to(string $path = null, $options = null): string
|
||||
public static function to(string|null $path = null, array|string|null $options = null): string
|
||||
{
|
||||
$kirby = App::instance();
|
||||
return ($kirby->component('url'))($kirby, $path, $options);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue