initial commit
This commit is contained in:
commit
9439de0603
600 changed files with 124248 additions and 0 deletions
826
kirby/src/Api/Api.php
Normal file
826
kirby/src/Api/Api.php
Normal file
|
|
@ -0,0 +1,826 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Api;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Http\Router;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Pagination;
|
||||
use Kirby\Toolkit\Properties;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The API class is a generic container
|
||||
* for API routes, models and collections and is used
|
||||
* to run our REST API. You can find our API setup
|
||||
* in `kirby/config/api.php`.
|
||||
*
|
||||
* @package Kirby Api
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Api
|
||||
{
|
||||
use Properties;
|
||||
|
||||
/**
|
||||
* Authentication callback
|
||||
*
|
||||
* @var Closure
|
||||
*/
|
||||
protected $authentication;
|
||||
|
||||
/**
|
||||
* Debugging flag
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $debug = false;
|
||||
|
||||
/**
|
||||
* Collection definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $collections = [];
|
||||
|
||||
/**
|
||||
* Injected data/dependencies
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Model definitions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $models = [];
|
||||
|
||||
/**
|
||||
* The current route
|
||||
*
|
||||
* @var Route
|
||||
*/
|
||||
protected $route;
|
||||
|
||||
/**
|
||||
* The Router instance
|
||||
*
|
||||
* @var Router
|
||||
*/
|
||||
protected $router;
|
||||
|
||||
/**
|
||||
* Route definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $routes = [];
|
||||
|
||||
/**
|
||||
* Request data
|
||||
* [query, body, files]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $requestData = [];
|
||||
|
||||
/**
|
||||
* The applied request method
|
||||
* (GET, POST, PATCH, etc.)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $requestMethod;
|
||||
|
||||
/**
|
||||
* Magic accessor for any given data
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $args = [])
|
||||
{
|
||||
return $this->data($method, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new API instance
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the authentication method
|
||||
* if set
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function authenticate()
|
||||
{
|
||||
if ($auth = $this->authentication()) {
|
||||
return $auth->call($this);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the authentication callback
|
||||
*
|
||||
* @return Closure|null
|
||||
*/
|
||||
public function authentication()
|
||||
{
|
||||
return $this->authentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an API call for the given path,
|
||||
* request method and optional request data
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $method
|
||||
* @param array $requestData
|
||||
* @return mixed
|
||||
*/
|
||||
public function call(string $path = null, string $method = 'GET', array $requestData = [])
|
||||
{
|
||||
$path = rtrim($path, '/');
|
||||
|
||||
$this->setRequestMethod($method);
|
||||
$this->setRequestData($requestData);
|
||||
|
||||
$this->router = new Router($this->routes());
|
||||
$this->route = $this->router->find($path, $method);
|
||||
$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) {
|
||||
$language = $user->language();
|
||||
|
||||
// get the locale from the translation
|
||||
$translation = $user->kirby()->translation($language);
|
||||
$locale = ($translation !== null)? $translation->locale() : $language;
|
||||
|
||||
// provide some variants as fallbacks to be
|
||||
// compatible with as many systems as possible
|
||||
$locales = [
|
||||
$locale . '.UTF-8',
|
||||
$locale . '.UTF8',
|
||||
$locale . '.ISO8859-1',
|
||||
$locale,
|
||||
$language,
|
||||
setlocale(LC_ALL, 0) // fall back to the previously defined locale
|
||||
];
|
||||
|
||||
// set the locales that are relevant for string formatting
|
||||
// *don't* set LC_CTYPE to avoid breaking other parts of the system
|
||||
setlocale(LC_MONETARY, $locales);
|
||||
setlocale(LC_NUMERIC, $locales);
|
||||
setlocale(LC_TIME, $locales);
|
||||
}
|
||||
}
|
||||
|
||||
// don't throw pagination errors if pagination
|
||||
// page is out of bounds
|
||||
$validate = Pagination::$validate;
|
||||
Pagination::$validate = false;
|
||||
|
||||
$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) {
|
||||
return $this->resolve($output)->toResponse();
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function collection(string $name, $collection = null)
|
||||
{
|
||||
if (isset($this->collections[$name]) === false) {
|
||||
throw new NotFoundException(sprintf('The collection "%s" does not exist', $name));
|
||||
}
|
||||
|
||||
return new Collection($this, $collection, $this->collections[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the collections definition
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function collections(): array
|
||||
{
|
||||
return $this->collections;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
if ($this->hasData($key) === false) {
|
||||
throw new NotFoundException(sprintf('Api data for "%s" does not exist', $key));
|
||||
}
|
||||
|
||||
// lazy-load data wrapped in Closures
|
||||
if (is_a($this->data[$key], 'Closure') === true) {
|
||||
return $this->data[$key]->call($this, ...$args);
|
||||
}
|
||||
|
||||
return $this->data[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the debugging flag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function debug(): bool
|
||||
{
|
||||
return $this->debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if injected data exists for the given key
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function hasData(string $key): bool
|
||||
{
|
||||
return isset($this->data[$key]) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches an object with an array item
|
||||
* based on the `type` field
|
||||
*
|
||||
* @param array models or collections
|
||||
* @param mixed $object
|
||||
*
|
||||
* @return string key of match
|
||||
*/
|
||||
protected function match(array $array, $object = null)
|
||||
{
|
||||
foreach ($array as $definition => $model) {
|
||||
if (is_a($object, $model['type']) === true) {
|
||||
return $definition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an API model instance by name
|
||||
*
|
||||
* @param string $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)
|
||||
{
|
||||
// Try to auto-match object with API models
|
||||
if ($name === null) {
|
||||
if ($model = $this->match($this->models, $object)) {
|
||||
$name = $model;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->models[$name]) === false) {
|
||||
throw new NotFoundException(sprintf('The model "%s" does not exist', $name));
|
||||
}
|
||||
|
||||
return new Model($this, $object, $this->models[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all model definitions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function models(): array
|
||||
{
|
||||
return $this->models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for request data
|
||||
* Can either get all the data
|
||||
* or certain parts of it.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestData(string $type = null, string $key = null, $default = null)
|
||||
{
|
||||
if ($type === null) {
|
||||
return $this->requestData;
|
||||
}
|
||||
|
||||
if ($key === null) {
|
||||
return $this->requestData[$type] ?? [];
|
||||
}
|
||||
|
||||
$data = array_change_key_case($this->requestData($type));
|
||||
$key = strtolower($key);
|
||||
|
||||
return $data[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request body if available
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestBody(string $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('body', $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the files from the request if available
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestFiles(string $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('files', $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all headers from the request if available
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestHeaders(string $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('headers', $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request method
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function requestMethod(): string
|
||||
{
|
||||
return $this->requestMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request query if available
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function requestQuery(string $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('query', $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
if (is_a($object, 'Kirby\Api\Model') === true || is_a($object, 'Kirby\Api\Collection') === true) {
|
||||
return $object;
|
||||
}
|
||||
|
||||
if ($model = $this->match($this->models, $object)) {
|
||||
return $this->model($model, $object);
|
||||
}
|
||||
|
||||
if ($collection = $this->match($this->collections, $object)) {
|
||||
return $this->collection($collection, $object);
|
||||
}
|
||||
|
||||
throw new NotFoundException(sprintf('The object "%s" cannot be resolved', get_class($object)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all defined routes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function routes(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the authentication callback
|
||||
*
|
||||
* @param Closure $authentication
|
||||
* @return self
|
||||
*/
|
||||
protected function setAuthentication(Closure $authentication = null)
|
||||
{
|
||||
$this->authentication = $authentication;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the collections definition
|
||||
*
|
||||
* @param array $collections
|
||||
* @return self
|
||||
*/
|
||||
protected function setCollections(array $collections = null)
|
||||
{
|
||||
if ($collections !== null) {
|
||||
$this->collections = array_change_key_case($collections);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the injected data
|
||||
*
|
||||
* @param array $data
|
||||
* @return self
|
||||
*/
|
||||
protected function setData(array $data = null)
|
||||
{
|
||||
$this->data = $data ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the debug flag
|
||||
*
|
||||
* @param bool $debug
|
||||
* @return self
|
||||
*/
|
||||
protected function setDebug(bool $debug = false)
|
||||
{
|
||||
$this->debug = $debug;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the model definitions
|
||||
*
|
||||
* @param array $models
|
||||
* @return self
|
||||
*/
|
||||
protected function setModels(array $models = null)
|
||||
{
|
||||
if ($models !== null) {
|
||||
$this->models = array_change_key_case($models);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the request data
|
||||
*
|
||||
* @param array $requestData
|
||||
* @return self
|
||||
*/
|
||||
protected function setRequestData(array $requestData = null)
|
||||
{
|
||||
$defaults = [
|
||||
'query' => [],
|
||||
'body' => [],
|
||||
'files' => []
|
||||
];
|
||||
|
||||
$this->requestData = array_merge($defaults, (array)$requestData);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the request method
|
||||
*
|
||||
* @param string $requestMethod
|
||||
* @return self
|
||||
*/
|
||||
protected function setRequestMethod(string $requestMethod = null)
|
||||
{
|
||||
$this->requestMethod = $requestMethod ?? 'GET';
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the route definitions
|
||||
*
|
||||
* @param array $routes
|
||||
* @return self
|
||||
*/
|
||||
protected function setRoutes(array $routes = null)
|
||||
{
|
||||
$this->routes = $routes ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the API call
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $method
|
||||
* @param array $requestData
|
||||
* @return mixed
|
||||
*/
|
||||
public function render(string $path, $method = 'GET', array $requestData = [])
|
||||
{
|
||||
try {
|
||||
$result = $this->call($path, $method, $requestData);
|
||||
} catch (Throwable $e) {
|
||||
$result = $this->responseForException($e);
|
||||
}
|
||||
|
||||
if ($result === null) {
|
||||
$result = $this->responseFor404();
|
||||
} elseif ($result === false) {
|
||||
$result = $this->responseFor400();
|
||||
} elseif ($result === true) {
|
||||
$result = $this->responseFor200();
|
||||
}
|
||||
|
||||
if (is_array($result) === false) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// pretty print json data
|
||||
$pretty = (bool)($requestData['query']['pretty'] ?? false) === true;
|
||||
|
||||
if (($result['status'] ?? 'ok') === 'error') {
|
||||
$code = $result['code'] ?? 400;
|
||||
|
||||
// sanitize the error code
|
||||
if ($code < 400 || $code > 599) {
|
||||
$code = 500;
|
||||
}
|
||||
|
||||
return Response::json($result, $code, $pretty);
|
||||
}
|
||||
|
||||
return Response::json($result, 200, $pretty);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 200 - ok
|
||||
* response array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function responseFor200(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'message' => 'ok',
|
||||
'code' => 200
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 400 - bad request
|
||||
* response array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function responseFor400(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'bad request',
|
||||
'code' => 400,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a 404 - not found
|
||||
* response array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function responseFor404(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'not found',
|
||||
'code' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the response array for
|
||||
* an exception. Kirby exceptions will
|
||||
* have more information
|
||||
*
|
||||
* @param Exception $e
|
||||
* @return array
|
||||
*/
|
||||
public function responseForException($e): array
|
||||
{
|
||||
// prepare the result array for all exception types
|
||||
$result = [
|
||||
'status' => 'error',
|
||||
'message' => $e->getMessage(),
|
||||
'code' => empty($e->getCode()) === true ? 500 : $e->getCode(),
|
||||
'exception' => get_class($e),
|
||||
'key' => null,
|
||||
'file' => F::relativepath($e->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? null),
|
||||
'line' => $e->getLine(),
|
||||
'details' => [],
|
||||
'route' => $this->route ? $this->route->pattern() : null
|
||||
];
|
||||
|
||||
// extend the information for Kirby Exceptions
|
||||
if (is_a($e, 'Kirby\Exception\Exception') === true) {
|
||||
$result['key'] = $e->getKey();
|
||||
$result['details'] = $e->getDetails();
|
||||
$result['code'] = $e->getHttpCode();
|
||||
}
|
||||
|
||||
// remove critical info from the result set if
|
||||
// debug mode is switched off
|
||||
if ($this->debug !== true) {
|
||||
unset(
|
||||
$result['file'],
|
||||
$result['exception'],
|
||||
$result['line'],
|
||||
$result['route']
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload helper method
|
||||
*
|
||||
* @param Closure $callback
|
||||
* @param bool $single
|
||||
* @return array
|
||||
*
|
||||
* @throws \Exception If request has no files
|
||||
* @throws \Exception If there was an error with the upload
|
||||
*/
|
||||
public function upload(Closure $callback, $single = false): array
|
||||
{
|
||||
$trials = 0;
|
||||
$uploads = [];
|
||||
$errors = [];
|
||||
$files = $this->requestFiles();
|
||||
|
||||
// get error messages from translation
|
||||
$errorMessages = [
|
||||
UPLOAD_ERR_INI_SIZE => t('upload.error.iniSize'),
|
||||
UPLOAD_ERR_FORM_SIZE => t('upload.error.formSize'),
|
||||
UPLOAD_ERR_PARTIAL => t('upload.error.partial'),
|
||||
UPLOAD_ERR_NO_FILE => t('upload.error.noFile'),
|
||||
UPLOAD_ERR_NO_TMP_DIR => t('upload.error.tmpDir'),
|
||||
UPLOAD_ERR_CANT_WRITE => t('upload.error.cantWrite'),
|
||||
UPLOAD_ERR_EXTENSION => t('upload.error.extension')
|
||||
];
|
||||
|
||||
if (empty($files) === true) {
|
||||
$postMaxSize = Str::toBytes(ini_get('post_max_size'));
|
||||
$uploadMaxFileSize = Str::toBytes(ini_get('upload_max_filesize'));
|
||||
|
||||
if ($postMaxSize < $uploadMaxFileSize) {
|
||||
throw new Exception(t('upload.error.iniPostSize'));
|
||||
} else {
|
||||
throw new Exception(t('upload.error.noFiles'));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($files as $upload) {
|
||||
if (isset($upload['tmp_name']) === false && is_array($upload)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$trials++;
|
||||
|
||||
try {
|
||||
if ($upload['error'] !== 0) {
|
||||
$errorMessage = $errorMessages[$upload['error']] ?? t('upload.error.default');
|
||||
throw new Exception($errorMessage);
|
||||
}
|
||||
|
||||
// get the extension of the uploaded file
|
||||
$extension = F::extension($upload['name']);
|
||||
|
||||
// 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'])) {
|
||||
$mime = F::mime($upload['tmp_name']);
|
||||
$extension = F::mimeToExtension($mime);
|
||||
$filename = F::name($upload['name']) . '.' . $extension;
|
||||
} else {
|
||||
$filename = basename($upload['name']);
|
||||
}
|
||||
|
||||
$source = dirname($upload['tmp_name']) . '/' . uniqid() . '.' . $filename;
|
||||
|
||||
// move the file to a location including the extension,
|
||||
// for better mime detection
|
||||
if (move_uploaded_file($upload['tmp_name'], $source) === false) {
|
||||
throw new Exception(t('upload.error.cantMove'));
|
||||
}
|
||||
|
||||
$data = $callback($source, $filename);
|
||||
|
||||
if (is_object($data) === true) {
|
||||
$data = $this->resolve($data)->toArray();
|
||||
}
|
||||
|
||||
$uploads[$upload['name']] = $data;
|
||||
} catch (Exception $e) {
|
||||
$errors[$upload['name']] = $e->getMessage();
|
||||
}
|
||||
|
||||
if ($single === true) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// return a single upload response
|
||||
if ($trials === 1) {
|
||||
if (empty($errors) === false) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => current($errors)
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'data' => current($uploads)
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($errors) === false) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => 'ok',
|
||||
'data' => $uploads
|
||||
];
|
||||
}
|
||||
}
|
||||
132
kirby/src/Api/Collection.php
Normal file
132
kirby/src/Api/Collection.php
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Api;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Collection class is a wrapper
|
||||
* around our Kirby Collections and handles
|
||||
* stuff like pagination and proper JSON output
|
||||
* for collections in REST calls.
|
||||
*
|
||||
* @package Kirby Api
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Collection
|
||||
{
|
||||
protected $api;
|
||||
protected $data;
|
||||
protected $model;
|
||||
protected $select;
|
||||
protected $view;
|
||||
|
||||
public function __construct(Api $api, $data = null, array $schema)
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->data = $data;
|
||||
$this->model = $schema['model'] ?? null;
|
||||
$this->view = $schema['view'] ?? null;
|
||||
|
||||
if ($data === null) {
|
||||
if (is_a($schema['default'] ?? null, 'Closure') === false) {
|
||||
throw new Exception('Missing collection data');
|
||||
}
|
||||
|
||||
$this->data = $schema['default']->call($this->api);
|
||||
}
|
||||
|
||||
if (
|
||||
isset($schema['type']) === true &&
|
||||
is_a($this->data, $schema['type']) === false
|
||||
) {
|
||||
throw new Exception('Invalid collection type');
|
||||
}
|
||||
}
|
||||
|
||||
public function select($keys = null)
|
||||
{
|
||||
if ($keys === false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (is_string($keys)) {
|
||||
$keys = Str::split($keys);
|
||||
}
|
||||
|
||||
if ($keys !== null && is_array($keys) === false) {
|
||||
throw new Exception('Invalid select keys');
|
||||
}
|
||||
|
||||
$this->select = $keys;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($this->data as $item) {
|
||||
$model = $this->api->model($this->model, $item);
|
||||
|
||||
if ($this->view !== null) {
|
||||
$model = $model->view($this->view);
|
||||
}
|
||||
|
||||
if ($this->select !== null) {
|
||||
$model = $model->select($this->select);
|
||||
}
|
||||
|
||||
$result[] = $model->toArray();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function toResponse(): array
|
||||
{
|
||||
if ($query = $this->api->requestQuery('query')) {
|
||||
$this->data = $this->data->query($query);
|
||||
}
|
||||
|
||||
if (!$this->data->pagination()) {
|
||||
$this->data = $this->data->paginate([
|
||||
'page' => $this->api->requestQuery('page', 1),
|
||||
'limit' => $this->api->requestQuery('limit', 100)
|
||||
]);
|
||||
}
|
||||
|
||||
$pagination = $this->data->pagination();
|
||||
|
||||
if ($select = $this->api->requestQuery('select')) {
|
||||
$this->select($select);
|
||||
}
|
||||
|
||||
if ($view = $this->api->requestQuery('view')) {
|
||||
$this->view($view);
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'data' => $this->toArray(),
|
||||
'pagination' => [
|
||||
'page' => $pagination->page(),
|
||||
'total' => $pagination->total(),
|
||||
'offset' => $pagination->offset(),
|
||||
'limit' => $pagination->limit(),
|
||||
],
|
||||
'status' => 'ok',
|
||||
'type' => 'collection'
|
||||
];
|
||||
}
|
||||
|
||||
public function view(string $view)
|
||||
{
|
||||
$this->view = $view;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
198
kirby/src/Api/Model.php
Normal file
198
kirby/src/Api/Model.php
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Api;
|
||||
|
||||
use Exception;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The API Model class can be wrapped around any
|
||||
* kind of object. Each model defines a set of properties that
|
||||
* are availabel in REST calls. Those properties are defined as
|
||||
* simple Closures which are resolved on demand. This is inspired
|
||||
* by GraphQLs architecture and makes it possible to load
|
||||
* only the model data that is needed for the current API call.
|
||||
*
|
||||
* @package Kirby Api
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Model
|
||||
{
|
||||
protected $api;
|
||||
protected $data;
|
||||
protected $fields;
|
||||
protected $select;
|
||||
protected $views;
|
||||
|
||||
public function __construct(Api $api, $data = null, array $schema)
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->data = $data;
|
||||
$this->fields = $schema['fields'] ?? [];
|
||||
$this->select = $schema['select'] ?? null;
|
||||
$this->views = $schema['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) {
|
||||
throw new Exception('Missing model data');
|
||||
}
|
||||
|
||||
$this->data = $schema['default']->call($this->api);
|
||||
}
|
||||
|
||||
if (
|
||||
isset($schema['type']) === true &&
|
||||
is_a($this->data, $schema['type']) === false
|
||||
) {
|
||||
throw new Exception(sprintf('Invalid model type "%s" expected: "%s"', get_class($this->data), $schema['type']));
|
||||
}
|
||||
}
|
||||
|
||||
public function select($keys = null)
|
||||
{
|
||||
if ($keys === false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (is_string($keys)) {
|
||||
$keys = Str::split($keys);
|
||||
}
|
||||
|
||||
if ($keys !== null && is_array($keys) === false) {
|
||||
throw new Exception('Invalid select keys');
|
||||
}
|
||||
|
||||
$this->select = $keys;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function selection(): array
|
||||
{
|
||||
$select = $this->select;
|
||||
|
||||
if ($select === null) {
|
||||
$select = array_keys($this->fields);
|
||||
}
|
||||
|
||||
$selection = [];
|
||||
|
||||
foreach ($select as $key => $value) {
|
||||
if (is_int($key) === true) {
|
||||
$selection[$value] = [
|
||||
'view' => null,
|
||||
'select' => null
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($value) === true) {
|
||||
if ($value === 'any') {
|
||||
throw new Exception('Invalid sub view: "any"');
|
||||
}
|
||||
|
||||
$selection[$key] = [
|
||||
'view' => $value,
|
||||
'select' => null
|
||||
];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value) === true) {
|
||||
$selection[$key] = [
|
||||
'view' => null,
|
||||
'select' => $value
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $selection;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$select = $this->selection();
|
||||
$result = [];
|
||||
|
||||
foreach ($this->fields as $key => $resolver) {
|
||||
if (array_key_exists($key, $select) === false || is_a($resolver, 'Closure') === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $resolver->call($this->api, $this->data);
|
||||
|
||||
if (is_object($value)) {
|
||||
$value = $this->api->resolve($value);
|
||||
}
|
||||
|
||||
if (
|
||||
is_a($value, 'Kirby\Api\Collection') === true ||
|
||||
is_a($value, 'Kirby\Api\Model') === true
|
||||
) {
|
||||
$selection = $select[$key];
|
||||
|
||||
if ($subview = $selection['view']) {
|
||||
$value->view($subview);
|
||||
}
|
||||
|
||||
if ($subselect = $selection['select']) {
|
||||
$value->select($subselect);
|
||||
}
|
||||
|
||||
$value = $value->toArray();
|
||||
}
|
||||
|
||||
$result[$key] = $value;
|
||||
}
|
||||
|
||||
ksort($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function toResponse(): array
|
||||
{
|
||||
$model = $this;
|
||||
|
||||
if ($select = $this->api->requestQuery('select')) {
|
||||
$model = $model->select($select);
|
||||
}
|
||||
|
||||
if ($view = $this->api->requestQuery('view')) {
|
||||
$model = $model->view($view);
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'data' => $model->toArray(),
|
||||
'status' => 'ok',
|
||||
'type' => 'model'
|
||||
];
|
||||
}
|
||||
|
||||
public function view(string $name)
|
||||
{
|
||||
if ($name === 'any') {
|
||||
return $this->select(null);
|
||||
}
|
||||
|
||||
if (isset($this->views[$name]) === false) {
|
||||
$name = 'default';
|
||||
|
||||
// try to fall back to the default view at least
|
||||
if (isset($this->views[$name]) === false) {
|
||||
throw new Exception(sprintf('The view "%s" does not exist', $name));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->select($this->views[$name]);
|
||||
}
|
||||
}
|
||||
86
kirby/src/Cache/ApcuCache.php
Normal file
86
kirby/src/Cache/ApcuCache.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
use APCuIterator;
|
||||
|
||||
/**
|
||||
* APCu Cache Driver
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class ApcuCache extends Cache
|
||||
{
|
||||
/**
|
||||
* Determines if an item exists in the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(string $key): bool
|
||||
{
|
||||
return apcu_exists($this->key($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
return apcu_delete($this->key($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
return Value::fromJson(apcu_fetch($this->key($key)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
* returns whether the operation was successful
|
||||
*
|
||||
* <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
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
return apcu_store($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes));
|
||||
}
|
||||
}
|
||||
242
kirby/src/Cache/Cache.php
Normal file
242
kirby/src/Cache/Cache.php
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
/**
|
||||
* Cache foundation
|
||||
* This abstract class is used as
|
||||
* foundation for other cache drivers
|
||||
* by extending it
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
abstract class Cache
|
||||
{
|
||||
/**
|
||||
* Stores all options for the driver
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* Sets all parameters which are needed to connect to the cache storage
|
||||
*
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = $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
|
||||
*
|
||||
* <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
|
||||
*/
|
||||
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
|
||||
{
|
||||
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
|
||||
$value = $this->retrieve($key);
|
||||
|
||||
// check for a valid cache value
|
||||
if (!is_a($value, 'Kirby\Cache\Value')) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the expiration timestamp
|
||||
*
|
||||
* @param int $minutes
|
||||
* @return int
|
||||
*/
|
||||
protected function expiration(int $minutes = 0): int
|
||||
{
|
||||
// 0 = keep forever
|
||||
if ($minutes === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// calculate the time
|
||||
return time() + ($minutes * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
// get the Value object
|
||||
$value = $this->retrieve($key);
|
||||
|
||||
// check for a valid Value object
|
||||
if (!is_a($value, 'Kirby\Cache\Value')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// return the expires timestamp
|
||||
return $value->expires();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an item in the cache is expired
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function expired(string $key): bool
|
||||
{
|
||||
$expires = $this->expires($key);
|
||||
|
||||
if ($expires === null) {
|
||||
return false;
|
||||
} elseif (!is_int($expires)) {
|
||||
return true;
|
||||
} else {
|
||||
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
|
||||
*/
|
||||
public function created(string $key)
|
||||
{
|
||||
// get the Value object
|
||||
$value = $this->retrieve($key);
|
||||
|
||||
// check for a valid Value object
|
||||
if (!is_a($value, 'Kirby\Cache\Value')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// return the expires timestamp
|
||||
return $value->created();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alternate version for Cache::created($key)
|
||||
*
|
||||
* @param string $key
|
||||
* @return int|false
|
||||
*/
|
||||
public function modified(string $key)
|
||||
{
|
||||
return static::created($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an item exists in the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(string $key): bool
|
||||
{
|
||||
return $this->expired($key) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
* this needs to be defined by the driver
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract public function flush(): bool;
|
||||
|
||||
/**
|
||||
* Returns all passed cache options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function options(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
}
|
||||
166
kirby/src/Cache/FileCache.php
Normal file
166
kirby/src/Cache/FileCache.php
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* File System Cache Driver
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class FileCache extends Cache
|
||||
{
|
||||
/**
|
||||
* Full root including prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* Sets all parameters which are needed for the file cache
|
||||
*
|
||||
* @param array $options 'root' (required)
|
||||
* 'prefix' (default: none)
|
||||
* 'extension' (file extension for cache files, default: none)
|
||||
*/
|
||||
public function __construct(array $options)
|
||||
{
|
||||
$defaults = [
|
||||
'root' => null,
|
||||
'prefix' => null,
|
||||
'extension' => null
|
||||
];
|
||||
|
||||
parent::__construct(array_merge($defaults, $options));
|
||||
|
||||
// build the full root including prefix
|
||||
$this->root = $this->options['root'];
|
||||
if (empty($this->options['prefix']) === false) {
|
||||
$this->root .= '/' . $this->options['prefix'];
|
||||
}
|
||||
|
||||
// try to create the directory
|
||||
Dir::make($this->root, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full root including prefix
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path to a file for a given key
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function file(string $key): string
|
||||
{
|
||||
$file = $this->root . '/' . $key;
|
||||
|
||||
if (isset($this->options['extension'])) {
|
||||
return $file . '.' . $this->options['extension'];
|
||||
} else {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
* returns whether the operation was successful
|
||||
*
|
||||
* <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
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
$file = $this->file($key);
|
||||
|
||||
return F::write($file, (new Value($value, $minutes))->toJson());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$file = $this->file($key);
|
||||
|
||||
return Value::fromJson(F::read($file));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
// use the modification timestamp
|
||||
// as indicator when the cache has been created/overwritten
|
||||
clearstatcache();
|
||||
|
||||
// get the file for this cache key
|
||||
$file = $this->file($key);
|
||||
return file_exists($file) ? filemtime($this->file($key)) : 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
|
||||
{
|
||||
$file = $this->file($key);
|
||||
|
||||
if (is_file($file) === true) {
|
||||
return F::remove($file);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // @codeCoverageIgnore
|
||||
}
|
||||
}
|
||||
97
kirby/src/Cache/MemCached.php
Normal file
97
kirby/src/Cache/MemCached.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
/**
|
||||
* Memcached Driver
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class MemCached extends Cache
|
||||
{
|
||||
/**
|
||||
* store for the memache connection
|
||||
* @var Memcached
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* Sets all parameters which are needed to connect to Memcached
|
||||
*
|
||||
* @param array $options 'host' (default: localhost)
|
||||
* 'port' (default: 11211)
|
||||
* 'prefix' (default: null)
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$defaults = [
|
||||
'host' => 'localhost',
|
||||
'port' => 11211,
|
||||
'prefix' => null,
|
||||
];
|
||||
|
||||
parent::__construct(array_merge($defaults, $options));
|
||||
|
||||
$this->connection = new \Memcached();
|
||||
$this->connection->addServer($this->options['host'], $this->options['port']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
* returns whether the operation was successful
|
||||
*
|
||||
* <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
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
return Value::fromJson($this->connection->get($this->key($key)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
return $this->connection->delete($this->key($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return $this->connection->flush();
|
||||
}
|
||||
}
|
||||
82
kirby/src/Cache/MemoryCache.php
Normal file
82
kirby/src/Cache/MemoryCache.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
/**
|
||||
* Memory Cache Driver (cache in memory for current request only)
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class MemoryCache extends Cache
|
||||
{
|
||||
/**
|
||||
* Cache data
|
||||
* @var array
|
||||
*/
|
||||
protected $store = [];
|
||||
|
||||
/**
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
* returns whether the operation was successful
|
||||
*
|
||||
* <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
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
$this->store[$key] = new Value($value, $minutes);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
return $this->store[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
$this->store = [];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
69
kirby/src/Cache/NullCache.php
Normal file
69
kirby/src/Cache/NullCache.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
/**
|
||||
* Dummy Cache Driver (does not do any caching)
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class NullCache extends Cache
|
||||
{
|
||||
/**
|
||||
* Writes an item to the cache for a given number of minutes and
|
||||
* returns whether the operation was successful
|
||||
*
|
||||
* <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
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the entire cache and returns
|
||||
* whether the operation was successful
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
144
kirby/src/Cache/Value.php
Normal file
144
kirby/src/Cache/Value.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Cache Value
|
||||
* Stores the value, creation timestamp and expiration timestamp
|
||||
* and makes it possible to store all three with a single cache key
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Value
|
||||
{
|
||||
/**
|
||||
* Cached value
|
||||
* @var mixed
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* the number of minutes until the value expires
|
||||
* @var int
|
||||
*/
|
||||
protected $minutes;
|
||||
|
||||
/**
|
||||
* Creation timestamp
|
||||
* @var int
|
||||
*/
|
||||
protected $created;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param int $minutes the number of minutes until the value expires
|
||||
* @param int $created the unix timestamp when the value has been created
|
||||
*/
|
||||
public function __construct($value, int $minutes = 0, int $created = null)
|
||||
{
|
||||
$this->value = $value;
|
||||
$this->minutes = $minutes ?? 0;
|
||||
$this->created = $created ?? time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the creation date as UNIX timestamp
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function created(): int
|
||||
{
|
||||
return $this->created;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expiration date as UNIX timestamp or
|
||||
* null if the value never expires
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function expires(): ?int
|
||||
{
|
||||
// 0 = keep forever
|
||||
if ($this->minutes === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->created + ($this->minutes * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a value object from an array
|
||||
*
|
||||
* @param array $array
|
||||
* @return self
|
||||
*/
|
||||
public static function fromArray(array $array)
|
||||
{
|
||||
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 self|null
|
||||
*/
|
||||
public static function fromJson(string $json)
|
||||
{
|
||||
try {
|
||||
$array = json_decode($json, true);
|
||||
|
||||
if (is_array($array)) {
|
||||
return static::fromArray($array);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object to a JSON string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'created' => $this->created,
|
||||
'minutes' => $this->minutes,
|
||||
'value' => $this->value,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pure value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function value()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
272
kirby/src/Cms/Api.php
Normal file
272
kirby/src/Cms/Api.php
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Api\Api as BaseApi;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Api
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Api extends BaseApi
|
||||
{
|
||||
/**
|
||||
* @var App
|
||||
*/
|
||||
protected $kirby;
|
||||
|
||||
/**
|
||||
* Execute an API call for the given path,
|
||||
* request method and optional request data
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $method
|
||||
* @param array $requestData
|
||||
* @return mixed
|
||||
*/
|
||||
public function call(string $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)) {
|
||||
$this->kirby->setCurrentTranslation($user->language());
|
||||
}
|
||||
|
||||
return parent::call($path, $method, $requestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $model
|
||||
* @param string $name
|
||||
* @param string $path
|
||||
* @return mixed
|
||||
*/
|
||||
public function fieldApi($model, string $name, string $path = null)
|
||||
{
|
||||
$form = Form::for($model);
|
||||
$fieldNames = Str::split($name, '+');
|
||||
$index = 0;
|
||||
$count = count($fieldNames);
|
||||
$field = null;
|
||||
|
||||
foreach ($fieldNames as $fieldName) {
|
||||
$index++;
|
||||
|
||||
if ($field = $form->fields()->get($fieldName)) {
|
||||
if ($count !== $index) {
|
||||
$form = $field->form();
|
||||
}
|
||||
} else {
|
||||
throw new NotFoundException('The field "' . $fieldName . '" could not be found');
|
||||
}
|
||||
}
|
||||
|
||||
if ($field === null) {
|
||||
throw new NotFoundException('The field "' . $fieldNames . '" could not be found');
|
||||
}
|
||||
|
||||
$fieldApi = $this->clone([
|
||||
'routes' => $field->api(),
|
||||
'data' => array_merge($this->data(), ['field' => $field])
|
||||
]);
|
||||
|
||||
return $fieldApi->call($path, $this->requestMethod(), $this->requestData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file object for the given
|
||||
* parent path and filename
|
||||
*
|
||||
* @param string $path Path to file's parent model
|
||||
* @param string $filename Filename
|
||||
* @return \Kirby\Cms\File|null
|
||||
*/
|
||||
public function file(string $path = null, string $filename)
|
||||
{
|
||||
$filename = urldecode($filename);
|
||||
$file = $this->parent($path)->file($filename);
|
||||
|
||||
if ($file && $file->isReadable() === true) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => 'file.notFound',
|
||||
'data' => [
|
||||
'filename' => $filename
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model's object for the given path
|
||||
*
|
||||
* @param string $path Path to parent model
|
||||
* @return \Kirby\Cms\Model|null
|
||||
*/
|
||||
public function parent(string $path)
|
||||
{
|
||||
$modelType = in_array($path, ['site', 'account']) ? $path : trim(dirname($path), '/');
|
||||
$modelTypes = [
|
||||
'site' => 'site',
|
||||
'users' => 'user',
|
||||
'pages' => 'page',
|
||||
'account' => 'account'
|
||||
];
|
||||
$modelName = $modelTypes[$modelType] ?? null;
|
||||
|
||||
if (Str::endsWith($modelType, '/files') === true) {
|
||||
$modelName = 'file';
|
||||
}
|
||||
|
||||
$kirby = $this->kirby();
|
||||
|
||||
switch ($modelName) {
|
||||
case 'site':
|
||||
$model = $kirby->site();
|
||||
break;
|
||||
case 'account':
|
||||
$model = $kirby->user(null, $kirby->option('api.allowImpersonation', false));
|
||||
break;
|
||||
case 'page':
|
||||
$id = str_replace(['+', ' '], '/', basename($path));
|
||||
$model = $kirby->page($id);
|
||||
break;
|
||||
case 'file':
|
||||
$model = $this->file(...explode('/files/', $path));
|
||||
break;
|
||||
case 'user':
|
||||
$model = $kirby->user(basename($path));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid file model type: ' . $modelType);
|
||||
}
|
||||
|
||||
if ($model) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => $modelName . '.undefined'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Kirby instance
|
||||
*
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
{
|
||||
return $this->kirby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language request header
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function language(): ?string
|
||||
{
|
||||
return get('language') ?? $this->requestHeaders('x-language');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the page object for the given id
|
||||
*
|
||||
* @param string $id Page's id
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function page(string $id)
|
||||
{
|
||||
$id = str_replace('+', '/', $id);
|
||||
$page = $this->kirby->page($id);
|
||||
|
||||
if ($page && $page->isReadable() === true) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => 'page.notFound',
|
||||
'data' => [
|
||||
'slug' => $id
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function session(array $options = [])
|
||||
{
|
||||
return $this->kirby->session(array_merge([
|
||||
'detect' => true
|
||||
], $options));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
*/
|
||||
protected function setKirby(App $kirby)
|
||||
{
|
||||
$this->kirby = $kirby;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the site object
|
||||
*
|
||||
* @return \Kirby\Cms\Site
|
||||
*/
|
||||
public function site()
|
||||
{
|
||||
return $this->kirby->site();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user object for the given id or
|
||||
* returns the current authenticated user if no
|
||||
* id is passed
|
||||
*
|
||||
* @param string $id User's id
|
||||
* @return \Kirby\Cms\User|null
|
||||
*/
|
||||
public function user(string $id = null)
|
||||
{
|
||||
// get the authenticated user
|
||||
if ($id === null) {
|
||||
return $this->kirby->auth()->user(null, $this->kirby()->option('api.allowImpersonation', false));
|
||||
}
|
||||
|
||||
// get a specific user by id
|
||||
if ($user = $this->kirby->users()->find($id)) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $id
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the users collection
|
||||
*
|
||||
* @return \Kirby\Cms\Users
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
return $this->kirby->users();
|
||||
}
|
||||
}
|
||||
1491
kirby/src/Cms/App.php
Normal file
1491
kirby/src/Cms/App.php
Normal file
File diff suppressed because it is too large
Load diff
138
kirby/src/Cms/AppCaches.php
Normal file
138
kirby/src/Cms/AppCaches.php
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Cache\Cache;
|
||||
use Kirby\Cache\NullCache;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* AppCaches
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait AppCaches
|
||||
{
|
||||
protected $caches = [];
|
||||
|
||||
/**
|
||||
* Returns a cache instance by key
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cache\Cache
|
||||
*/
|
||||
public function cache(string $key)
|
||||
{
|
||||
if (isset($this->caches[$key]) === true) {
|
||||
return $this->caches[$key];
|
||||
}
|
||||
|
||||
// get the options for this cache type
|
||||
$options = $this->cacheOptions($key);
|
||||
|
||||
if ($options['active'] === false) {
|
||||
// use a dummy cache that does nothing
|
||||
return $this->caches[$key] = new NullCache();
|
||||
}
|
||||
|
||||
$type = strtolower($options['type']);
|
||||
$types = $this->extensions['cacheTypes'] ?? [];
|
||||
|
||||
if (array_key_exists($type, $types) === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'app.invalid.cacheType',
|
||||
'data' => ['type' => $type]
|
||||
]);
|
||||
}
|
||||
|
||||
$className = $types[$type];
|
||||
|
||||
// initialize the cache class
|
||||
$cache = new $className($options);
|
||||
|
||||
// check if it is a useable cache object
|
||||
if (is_a($cache, 'Kirby\Cache\Cache') !== true) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'app.invalid.cacheType',
|
||||
'data' => ['type' => $type]
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->caches[$key] = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cache options by key
|
||||
*
|
||||
* @param string $key
|
||||
* @return array
|
||||
*/
|
||||
protected function cacheOptions(string $key): array
|
||||
{
|
||||
$options = $this->option($cacheKey = $this->cacheOptionsKey($key), false);
|
||||
|
||||
if ($options === false) {
|
||||
return [
|
||||
'active' => false
|
||||
];
|
||||
}
|
||||
|
||||
$prefix = str_replace(['/', ':'], '_', $this->system()->indexUrl()) .
|
||||
'/' .
|
||||
str_replace('.', '/', $key);
|
||||
|
||||
$defaults = [
|
||||
'active' => true,
|
||||
'type' => 'file',
|
||||
'extension' => 'cache',
|
||||
'root' => $this->root('cache'),
|
||||
'prefix' => $prefix
|
||||
];
|
||||
|
||||
if ($options === true) {
|
||||
return $defaults;
|
||||
} else {
|
||||
return array_merge($defaults, $options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes care of converting prefixed plugin cache setups
|
||||
* to the right cache key, while leaving regular cache
|
||||
* setups untouched.
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function cacheOptionsKey(string $key): string
|
||||
{
|
||||
$prefixedKey = 'cache.' . $key;
|
||||
|
||||
if (isset($this->options[$prefixedKey])) {
|
||||
return $prefixedKey;
|
||||
}
|
||||
|
||||
// plain keys without dots don't need further investigation
|
||||
// since they can never be from a plugin.
|
||||
if (strpos($key, '.') === false) {
|
||||
return $prefixedKey;
|
||||
}
|
||||
|
||||
// try to extract the plugin name
|
||||
$parts = explode('.', $key);
|
||||
$pluginName = implode('/', array_slice($parts, 0, 2));
|
||||
$pluginPrefix = implode('.', array_slice($parts, 0, 2));
|
||||
$cacheName = implode('.', array_slice($parts, 2));
|
||||
|
||||
// check if such a plugin exists
|
||||
if ($this->plugin($pluginName)) {
|
||||
return empty($cacheName) === true ? $pluginPrefix . '.cache' : $pluginPrefix . '.cache.' . $cacheName;
|
||||
}
|
||||
|
||||
return $prefixedKey;
|
||||
}
|
||||
}
|
||||
184
kirby/src/Cms/AppErrors.php
Normal file
184
kirby/src/Cms/AppErrors.php
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Http\Response;
|
||||
use Whoops\Handler\CallbackHandler;
|
||||
use Whoops\Handler\Handler;
|
||||
use Whoops\Handler\PlainTextHandler;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Run as Whoops;
|
||||
|
||||
/**
|
||||
* PHP error handling using the Whoops library
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait AppErrors
|
||||
{
|
||||
/**
|
||||
* Whoops instance cache
|
||||
*
|
||||
* @var \Whoops\Run
|
||||
*/
|
||||
protected $whoops;
|
||||
|
||||
/**
|
||||
* Registers the PHP error handler for CLI usage
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function handleCliErrors(): void
|
||||
{
|
||||
$this->setWhoopsHandler(new PlainTextHandler());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the PHP error handler
|
||||
* based on the environment
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function handleErrors(): void
|
||||
{
|
||||
if ($this->request()->cli() === true) {
|
||||
$this->handleCliErrors();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->visitor()->prefersJson() === true) {
|
||||
$this->handleJsonErrors();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->handleHtmlErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the PHP error handler for HTML output
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function handleHtmlErrors(): void
|
||||
{
|
||||
$handler = null;
|
||||
|
||||
if ($this->option('debug') === true) {
|
||||
if ($this->option('whoops', true) === true) {
|
||||
$handler = new PrettyPageHandler();
|
||||
$handler->setPageTitle('Kirby CMS Debugger');
|
||||
$handler->setResourcesPath(dirname(__DIR__, 2) . '/assets');
|
||||
$handler->addCustomCss('whoops.css');
|
||||
|
||||
if ($editor = $this->option('editor')) {
|
||||
$handler->setEditor($editor);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$handler = new CallbackHandler(function ($exception, $inspector, $run) {
|
||||
$fatal = $this->option('fatal');
|
||||
|
||||
if (is_a($fatal, 'Closure') === true) {
|
||||
echo $fatal($this);
|
||||
} else {
|
||||
include $this->root('kirby') . '/views/fatal.php';
|
||||
}
|
||||
|
||||
return Handler::QUIT;
|
||||
});
|
||||
}
|
||||
|
||||
if ($handler !== null) {
|
||||
$this->setWhoopsHandler($handler);
|
||||
} else {
|
||||
$this->unsetWhoopsHandler();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the PHP error handler for JSON output
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function handleJsonErrors(): void
|
||||
{
|
||||
$handler = new CallbackHandler(function ($exception, $inspector, $run) {
|
||||
if (is_a($exception, 'Kirby\Exception\Exception') === true) {
|
||||
$httpCode = $exception->getHttpCode();
|
||||
$code = $exception->getCode();
|
||||
$details = $exception->getDetails();
|
||||
} else {
|
||||
$httpCode = 500;
|
||||
$code = $exception->getCode();
|
||||
$details = null;
|
||||
}
|
||||
|
||||
if ($this->option('debug') === true) {
|
||||
echo Response::json([
|
||||
'status' => 'error',
|
||||
'exception' => get_class($exception),
|
||||
'code' => $code,
|
||||
'message' => $exception->getMessage(),
|
||||
'details' => $details,
|
||||
'file' => ltrim($exception->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? null),
|
||||
'line' => $exception->getLine(),
|
||||
], $httpCode);
|
||||
} else {
|
||||
echo Response::json([
|
||||
'status' => 'error',
|
||||
'code' => $code,
|
||||
'details' => $details,
|
||||
'message' => 'An unexpected error occurred! Enable debug mode for more info: https://getkirby.com/docs/reference/system/options/debug',
|
||||
], $httpCode);
|
||||
}
|
||||
|
||||
return Handler::QUIT;
|
||||
});
|
||||
|
||||
$this->setWhoopsHandler($handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables Whoops with the specified handler
|
||||
*
|
||||
* @param Callable|\Whoops\Handler\HandlerInterface $handler
|
||||
* @return void
|
||||
*/
|
||||
protected function setWhoopsHandler($handler): void
|
||||
{
|
||||
$whoops = $this->whoops();
|
||||
$whoops->clearHandlers();
|
||||
$whoops->pushHandler($handler);
|
||||
$whoops->register(); // will only do something if not already registered
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the Whoops handlers and disables Whoops
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function unsetWhoopsHandler(): void
|
||||
{
|
||||
$whoops = $this->whoops();
|
||||
$whoops->clearHandlers();
|
||||
$whoops->unregister(); // will only do something if currently registered
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Whoops error handler instance
|
||||
*
|
||||
* @return \Whoops\Run
|
||||
*/
|
||||
protected function whoops()
|
||||
{
|
||||
if ($this->whoops !== null) {
|
||||
return $this->whoops;
|
||||
}
|
||||
|
||||
return $this->whoops = new Whoops();
|
||||
}
|
||||
}
|
||||
805
kirby/src/Cms/AppPlugins.php
Normal file
805
kirby/src/Cms/AppPlugins.php
Normal file
|
|
@ -0,0 +1,805 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Form\Field as FormField;
|
||||
use Kirby\Text\KirbyTag;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Collection as ToolkitCollection;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
/**
|
||||
* AppPlugins
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait AppPlugins
|
||||
{
|
||||
/**
|
||||
* A list of all registered plugins
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $plugins = [];
|
||||
|
||||
/**
|
||||
* Cache for system extensions
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $systemExtensions = null;
|
||||
|
||||
/**
|
||||
* The extension registry
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $extensions = [
|
||||
// load options first to make them available for the rest
|
||||
'options' => [],
|
||||
|
||||
// other plugin types
|
||||
'api' => [],
|
||||
'blueprints' => [],
|
||||
'cacheTypes' => [],
|
||||
'collections' => [],
|
||||
'components' => [],
|
||||
'controllers' => [],
|
||||
'collectionFilters' => [],
|
||||
'collectionMethods' => [],
|
||||
'fieldMethods' => [],
|
||||
'fileMethods' => [],
|
||||
'filesMethods' => [],
|
||||
'fields' => [],
|
||||
'hooks' => [],
|
||||
'pages' => [],
|
||||
'pageMethods' => [],
|
||||
'pagesMethods' => [],
|
||||
'pageModels' => [],
|
||||
'permissions' => [],
|
||||
'routes' => [],
|
||||
'sections' => [],
|
||||
'siteMethods' => [],
|
||||
'snippets' => [],
|
||||
'tags' => [],
|
||||
'templates' => [],
|
||||
'thirdParty' => [],
|
||||
'translations' => [],
|
||||
'userMethods' => [],
|
||||
'userModels' => [],
|
||||
'usersMethods' => [],
|
||||
'validators' => []
|
||||
];
|
||||
|
||||
/**
|
||||
* Flag when plugins have been loaded
|
||||
* to not load them again
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $pluginsAreLoaded = false;
|
||||
|
||||
/**
|
||||
* Register all given extensions
|
||||
*
|
||||
* @internal
|
||||
* @param array $extensions
|
||||
* @param \Kirby\Cms\Plugin $plugin The plugin which defined those extensions
|
||||
* @return array
|
||||
*/
|
||||
public function extend(array $extensions, Plugin $plugin = null): array
|
||||
{
|
||||
foreach ($this->extensions as $type => $registered) {
|
||||
if (isset($extensions[$type]) === true) {
|
||||
$this->{'extend' . $type}($extensions[$type], $plugin);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers API extensions
|
||||
*
|
||||
* @param array|bool $api
|
||||
* @return array
|
||||
*/
|
||||
protected function extendApi($api): array
|
||||
{
|
||||
if (is_array($api) === true) {
|
||||
if (is_a($api['routes'] ?? [], 'Closure') === true) {
|
||||
$api['routes'] = $api['routes']($this);
|
||||
}
|
||||
|
||||
return $this->extensions['api'] = A::merge($this->extensions['api'], $api, A::MERGE_APPEND);
|
||||
} else {
|
||||
return $this->extensions['api'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional blueprints
|
||||
*
|
||||
* @param array $blueprints
|
||||
* @return array
|
||||
*/
|
||||
protected function extendBlueprints(array $blueprints): array
|
||||
{
|
||||
return $this->extensions['blueprints'] = array_merge($this->extensions['blueprints'], $blueprints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional cache types
|
||||
*
|
||||
* @param array $cacheTypes
|
||||
* @return array
|
||||
*/
|
||||
protected function extendCacheTypes(array $cacheTypes): array
|
||||
{
|
||||
return $this->extensions['cacheTypes'] = array_merge($this->extensions['cacheTypes'], $cacheTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional collection filters
|
||||
*
|
||||
* @param array $filters
|
||||
* @return array
|
||||
*/
|
||||
protected function extendCollectionFilters(array $filters): array
|
||||
{
|
||||
return $this->extensions['collectionFilters'] = ToolkitCollection::$filters = array_merge(ToolkitCollection::$filters, $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional collection methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendCollectionMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['collectionMethods'] = Collection::$methods = array_merge(Collection::$methods, $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional collections
|
||||
*
|
||||
* @param array $collections
|
||||
* @return array
|
||||
*/
|
||||
protected function extendCollections(array $collections): array
|
||||
{
|
||||
return $this->extensions['collections'] = array_merge($this->extensions['collections'], $collections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers core components
|
||||
*
|
||||
* @param array $components
|
||||
* @return array
|
||||
*/
|
||||
protected function extendComponents(array $components): array
|
||||
{
|
||||
return $this->extensions['components'] = array_merge($this->extensions['components'], $components);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional controllers
|
||||
*
|
||||
* @param array $controllers
|
||||
* @return array
|
||||
*/
|
||||
protected function extendControllers(array $controllers): array
|
||||
{
|
||||
return $this->extensions['controllers'] = array_merge($this->extensions['controllers'], $controllers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional file methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendFileMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['fileMethods'] = File::$methods = array_merge(File::$methods, $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional files methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendFilesMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['filesMethods'] = Files::$methods = array_merge(Files::$methods, $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional field methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendFieldMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['fieldMethods'] = Field::$methods = array_merge(Field::$methods, array_change_key_case($methods));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers Panel fields
|
||||
*
|
||||
* @param array $fields
|
||||
* @return array
|
||||
*/
|
||||
protected function extendFields(array $fields): array
|
||||
{
|
||||
return $this->extensions['fields'] = FormField::$types = array_merge(FormField::$types, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers hooks
|
||||
*
|
||||
* @param array $hooks
|
||||
* @return array
|
||||
*/
|
||||
protected function extendHooks(array $hooks): array
|
||||
{
|
||||
foreach ($hooks as $name => $callbacks) {
|
||||
if (isset($this->extensions['hooks'][$name]) === false) {
|
||||
$this->extensions['hooks'][$name] = [];
|
||||
}
|
||||
|
||||
if (is_array($callbacks) === false) {
|
||||
$callbacks = [$callbacks];
|
||||
}
|
||||
|
||||
foreach ($callbacks as $callback) {
|
||||
$this->extensions['hooks'][$name][] = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->extensions['hooks'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers markdown component
|
||||
*
|
||||
* @param Closure $markdown
|
||||
* @return Closure
|
||||
*/
|
||||
protected function extendMarkdown(Closure $markdown)
|
||||
{
|
||||
return $this->extensions['markdown'] = $markdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional options
|
||||
*
|
||||
* @param array $options
|
||||
* @param \Kirby\Cms\Plugin|null $plugin
|
||||
* @return array
|
||||
*/
|
||||
protected function extendOptions(array $options, Plugin $plugin = null): array
|
||||
{
|
||||
if ($plugin !== null) {
|
||||
$prefixed = [];
|
||||
|
||||
foreach ($options as $key => $value) {
|
||||
$prefixed[$plugin->prefix() . '.' . $key] = $value;
|
||||
}
|
||||
|
||||
$options = $prefixed;
|
||||
}
|
||||
|
||||
// register each option in the nesting blacklist;
|
||||
// this prevents Kirby from nesting the array keys inside each option
|
||||
static::$nestIgnoreOptions = array_merge(static::$nestIgnoreOptions, array_keys($options));
|
||||
|
||||
return $this->extensions['options'] = $this->options = A::merge($options, $this->options, A::MERGE_REPLACE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional page methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendPageMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['pageMethods'] = Page::$methods = array_merge(Page::$methods, $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional pages methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendPagesMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['pagesMethods'] = Pages::$methods = array_merge(Pages::$methods, $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional page models
|
||||
*
|
||||
* @param array $models
|
||||
* @return array
|
||||
*/
|
||||
protected function extendPageModels(array $models): array
|
||||
{
|
||||
return $this->extensions['pageModels'] = Page::$models = array_merge(Page::$models, $models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers pages
|
||||
*
|
||||
* @param array $pages
|
||||
* @return array
|
||||
*/
|
||||
protected function extendPages(array $pages): array
|
||||
{
|
||||
return $this->extensions['pages'] = array_merge($this->extensions['pages'], $pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional permissions
|
||||
*
|
||||
* @param array $permissions
|
||||
* @param \Kirby\Cms\Plugin|null $plugin
|
||||
* @return array
|
||||
*/
|
||||
protected function extendPermissions(array $permissions, Plugin $plugin = null): array
|
||||
{
|
||||
if ($plugin !== null) {
|
||||
$permissions = [$plugin->prefix() => $permissions];
|
||||
}
|
||||
|
||||
return $this->extensions['permissions'] = Permissions::$extendedActions = array_merge(Permissions::$extendedActions, $permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional routes
|
||||
*
|
||||
* @param array|Closure $routes
|
||||
* @return array
|
||||
*/
|
||||
protected function extendRoutes($routes): array
|
||||
{
|
||||
if (is_a($routes, 'Closure') === true) {
|
||||
$routes = $routes($this);
|
||||
}
|
||||
|
||||
return $this->extensions['routes'] = array_merge($this->extensions['routes'], $routes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers Panel sections
|
||||
*
|
||||
* @param array $sections
|
||||
* @return array
|
||||
*/
|
||||
protected function extendSections(array $sections): array
|
||||
{
|
||||
return $this->extensions['sections'] = Section::$types = array_merge(Section::$types, $sections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional site methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendSiteMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['siteMethods'] = Site::$methods = array_merge(Site::$methods, $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers SmartyPants component
|
||||
*
|
||||
* @param Closure $smartypants
|
||||
* @return Closure
|
||||
*/
|
||||
protected function extendSmartypants(Closure $smartypants)
|
||||
{
|
||||
return $this->extensions['smartypants'] = $smartypants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional snippets
|
||||
*
|
||||
* @param array $snippets
|
||||
* @return array
|
||||
*/
|
||||
protected function extendSnippets(array $snippets): array
|
||||
{
|
||||
return $this->extensions['snippets'] = array_merge($this->extensions['snippets'], $snippets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional KirbyTags
|
||||
*
|
||||
* @param array $tags
|
||||
* @return array
|
||||
*/
|
||||
protected function extendTags(array $tags): array
|
||||
{
|
||||
return $this->extensions['tags'] = KirbyTag::$types = array_merge(KirbyTag::$types, array_change_key_case($tags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional templates
|
||||
*
|
||||
* @param array $templates
|
||||
* @return array
|
||||
*/
|
||||
protected function extendTemplates(array $templates): array
|
||||
{
|
||||
return $this->extensions['templates'] = array_merge($this->extensions['templates'], $templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers translations
|
||||
*
|
||||
* @param array $translations
|
||||
* @return array
|
||||
*/
|
||||
protected function extendTranslations(array $translations): array
|
||||
{
|
||||
return $this->extensions['translations'] = array_replace_recursive($this->extensions['translations'], $translations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add third party extensions to the registry
|
||||
* so they can be used as plugins for plugins
|
||||
* for example.
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $extensions
|
||||
* @return array
|
||||
*/
|
||||
protected function extendThirdParty(array $extensions): array
|
||||
{
|
||||
return $this->extensions['thirdParty'] = array_replace_recursive($this->extensions['thirdParty'], $extensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional user methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendUserMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['userMethods'] = User::$methods = array_merge(User::$methods, $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional user models
|
||||
*
|
||||
* @param array $models
|
||||
* @return array
|
||||
*/
|
||||
protected function extendUserModels(array $models): array
|
||||
{
|
||||
return $this->extensions['userModels'] = User::$models = array_merge(User::$models, $models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional users methods
|
||||
*
|
||||
* @param array $methods
|
||||
* @return array
|
||||
*/
|
||||
protected function extendUsersMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['usersMethods'] = Users::$methods = array_merge(Users::$methods, $methods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional custom validators
|
||||
*
|
||||
* @param array $validators
|
||||
* @return array
|
||||
*/
|
||||
protected function extendValidators(array $validators): array
|
||||
{
|
||||
return $this->extensions['validators'] = V::$validators = array_merge(V::$validators, $validators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a given extension by type and name
|
||||
*
|
||||
* @internal
|
||||
* @param string $type i.e. `'hooks'`
|
||||
* @param string $name i.e. `'page.delete:before'`
|
||||
* @param mixed $fallback
|
||||
* @return mixed
|
||||
*/
|
||||
public function extension(string $type, string $name, $fallback = null)
|
||||
{
|
||||
return $this->extensions($type)[$name] ?? $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extensions registry
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $type
|
||||
* @return array
|
||||
*/
|
||||
public function extensions(string $type = null)
|
||||
{
|
||||
if ($type === null) {
|
||||
return $this->extensions;
|
||||
}
|
||||
|
||||
return $this->extensions[$type] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load extensions from site folders.
|
||||
* This is only used for models for now, but
|
||||
* could be extended later
|
||||
*/
|
||||
protected function extensionsFromFolders()
|
||||
{
|
||||
$models = [];
|
||||
|
||||
foreach (glob($this->root('models') . '/*.php') as $model) {
|
||||
$name = F::name($model);
|
||||
$class = str_replace(['.', '-', '_'], '', $name) . 'Page';
|
||||
|
||||
// load the model class
|
||||
F::loadOnce($model);
|
||||
|
||||
if (class_exists($class) === true) {
|
||||
$models[$name] = $class;
|
||||
}
|
||||
}
|
||||
|
||||
$this->extendPageModels($models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register extensions that could be located in
|
||||
* the options array. I.e. hooks and routes can be
|
||||
* setup from the config.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function extensionsFromOptions()
|
||||
{
|
||||
// register routes and hooks from options
|
||||
$this->extend([
|
||||
'api' => $this->options['api'] ?? [],
|
||||
'routes' => $this->options['routes'] ?? [],
|
||||
'hooks' => $this->options['hooks'] ?? []
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all plugin extensions
|
||||
*
|
||||
* @param array $plugins
|
||||
* @return void
|
||||
*/
|
||||
protected function extensionsFromPlugins()
|
||||
{
|
||||
// register all their extensions
|
||||
foreach ($this->plugins() as $plugin) {
|
||||
$extends = $plugin->extends();
|
||||
|
||||
if (empty($extends) === false) {
|
||||
$this->extend($extends, $plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all passed extensions
|
||||
*
|
||||
* @param array $props
|
||||
* @return void
|
||||
*/
|
||||
protected function extensionsFromProps(array $props)
|
||||
{
|
||||
$this->extend($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all default extensions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function extensionsFromSystem()
|
||||
{
|
||||
$root = $this->root('kirby');
|
||||
|
||||
// load static extensions only once
|
||||
if (static::$systemExtensions === null) {
|
||||
// Form Field Mixins
|
||||
FormField::$mixins['filepicker'] = include $root . '/config/fields/mixins/filepicker.php';
|
||||
FormField::$mixins['min'] = include $root . '/config/fields/mixins/min.php';
|
||||
FormField::$mixins['options'] = include $root . '/config/fields/mixins/options.php';
|
||||
FormField::$mixins['pagepicker'] = include $root . '/config/fields/mixins/pagepicker.php';
|
||||
FormField::$mixins['picker'] = include $root . '/config/fields/mixins/picker.php';
|
||||
FormField::$mixins['upload'] = include $root . '/config/fields/mixins/upload.php';
|
||||
FormField::$mixins['userpicker'] = include $root . '/config/fields/mixins/userpicker.php';
|
||||
|
||||
// Tag Aliases
|
||||
KirbyTag::$aliases = [
|
||||
'youtube' => 'video',
|
||||
'vimeo' => 'video'
|
||||
];
|
||||
|
||||
// Field method aliases
|
||||
Field::$aliases = [
|
||||
'bool' => 'toBool',
|
||||
'esc' => 'escape',
|
||||
'excerpt' => 'toExcerpt',
|
||||
'float' => 'toFloat',
|
||||
'h' => 'html',
|
||||
'int' => 'toInt',
|
||||
'kt' => 'kirbytext',
|
||||
'kti' => 'kirbytextinline',
|
||||
'link' => 'toLink',
|
||||
'md' => 'markdown',
|
||||
'sp' => 'smartypants',
|
||||
'v' => 'isValid',
|
||||
'x' => 'xml'
|
||||
];
|
||||
|
||||
// blueprint presets
|
||||
PageBlueprint::$presets['pages'] = include $root . '/config/presets/pages.php';
|
||||
PageBlueprint::$presets['page'] = include $root . '/config/presets/page.php';
|
||||
PageBlueprint::$presets['files'] = include $root . '/config/presets/files.php';
|
||||
|
||||
// section mixins
|
||||
Section::$mixins['empty'] = include $root . '/config/sections/mixins/empty.php';
|
||||
Section::$mixins['headline'] = include $root . '/config/sections/mixins/headline.php';
|
||||
Section::$mixins['help'] = include $root . '/config/sections/mixins/help.php';
|
||||
Section::$mixins['layout'] = include $root . '/config/sections/mixins/layout.php';
|
||||
Section::$mixins['max'] = include $root . '/config/sections/mixins/max.php';
|
||||
Section::$mixins['min'] = include $root . '/config/sections/mixins/min.php';
|
||||
Section::$mixins['pagination'] = include $root . '/config/sections/mixins/pagination.php';
|
||||
Section::$mixins['parent'] = include $root . '/config/sections/mixins/parent.php';
|
||||
|
||||
// section types
|
||||
Section::$types['info'] = include $root . '/config/sections/info.php';
|
||||
Section::$types['pages'] = include $root . '/config/sections/pages.php';
|
||||
Section::$types['files'] = include $root . '/config/sections/files.php';
|
||||
Section::$types['fields'] = include $root . '/config/sections/fields.php';
|
||||
|
||||
static::$systemExtensions = [
|
||||
'components' => include $root . '/config/components.php',
|
||||
'blueprints' => include $root . '/config/blueprints.php',
|
||||
'fields' => include $root . '/config/fields.php',
|
||||
'fieldMethods' => include $root . '/config/methods.php',
|
||||
'tags' => include $root . '/config/tags.php'
|
||||
];
|
||||
}
|
||||
|
||||
// default cache types
|
||||
$this->extendCacheTypes([
|
||||
'apcu' => 'Kirby\Cache\ApcuCache',
|
||||
'file' => 'Kirby\Cache\FileCache',
|
||||
'memcached' => 'Kirby\Cache\MemCached',
|
||||
'memory' => 'Kirby\Cache\MemoryCache',
|
||||
]);
|
||||
|
||||
$this->extendComponents(static::$systemExtensions['components']);
|
||||
$this->extendBlueprints(static::$systemExtensions['blueprints']);
|
||||
$this->extendFields(static::$systemExtensions['fields']);
|
||||
$this->extendFieldMethods((static::$systemExtensions['fieldMethods'])($this));
|
||||
$this->extendTags(static::$systemExtensions['tags']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the native implementation
|
||||
* of a core component
|
||||
*
|
||||
* @param string $component
|
||||
* @return \Closure | false
|
||||
*/
|
||||
public function nativeComponent(string $component)
|
||||
{
|
||||
return static::$systemExtensions['components'][$component] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kirby plugin factory and getter
|
||||
*
|
||||
* @param string $name
|
||||
* @param array|null $extends If null is passed it will be used as getter. Otherwise as factory.
|
||||
* @return \Kirby\Cms\Plugin|null
|
||||
*/
|
||||
public static function plugin(string $name, array $extends = null)
|
||||
{
|
||||
if ($extends === null) {
|
||||
return static::$plugins[$name] ?? null;
|
||||
}
|
||||
|
||||
// get the correct root for the plugin
|
||||
$extends['root'] = $extends['root'] ?? dirname(debug_backtrace()[0]['file']);
|
||||
|
||||
$plugin = new Plugin($name, $extends);
|
||||
$name = $plugin->name();
|
||||
|
||||
if (isset(static::$plugins[$name]) === true) {
|
||||
throw new DuplicateException('The plugin "' . $name . '" has already been registered');
|
||||
}
|
||||
|
||||
return static::$plugins[$name] = $plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns all plugins in the site/plugins directory
|
||||
* Loading only happens on the first call.
|
||||
*
|
||||
* @internal
|
||||
* @param array $plugins Can be used to overwrite the plugins registry
|
||||
* @return array
|
||||
*/
|
||||
public function plugins(array $plugins = null): array
|
||||
{
|
||||
// overwrite the existing plugins registry
|
||||
if ($plugins !== null) {
|
||||
$this->pluginsAreLoaded = true;
|
||||
return static::$plugins = $plugins;
|
||||
}
|
||||
|
||||
// don't load plugins twice
|
||||
if ($this->pluginsAreLoaded === true) {
|
||||
return static::$plugins;
|
||||
}
|
||||
|
||||
// load all plugins from site/plugins
|
||||
$this->pluginsLoader();
|
||||
|
||||
// mark plugins as loaded to stop doing it twice
|
||||
$this->pluginsAreLoaded = true;
|
||||
return static::$plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all plugins from site/plugins
|
||||
*
|
||||
* @return array Array of loaded directories
|
||||
*/
|
||||
protected function pluginsLoader(): array
|
||||
{
|
||||
$root = $this->root('plugins');
|
||||
$loaded = [];
|
||||
|
||||
foreach (Dir::read($root) as $dirname) {
|
||||
if (in_array(substr($dirname, 0, 1), ['.', '_']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dir = $root . '/' . $dirname;
|
||||
$entry = $dir . '/index.php';
|
||||
|
||||
if (is_dir($dir) !== true || is_file($entry) !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
F::loadOnce($entry);
|
||||
|
||||
$loaded[] = $dir;
|
||||
}
|
||||
|
||||
return $loaded;
|
||||
}
|
||||
}
|
||||
177
kirby/src/Cms/AppTranslations.php
Normal file
177
kirby/src/Cms/AppTranslations.php
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* AppTranslations
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait AppTranslations
|
||||
{
|
||||
protected $translations;
|
||||
|
||||
/**
|
||||
* Setup internationalization
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function i18n(): void
|
||||
{
|
||||
I18n::$load = function ($locale): array {
|
||||
$data = [];
|
||||
|
||||
if ($translation = $this->translation($locale)) {
|
||||
$data = $translation->data();
|
||||
}
|
||||
|
||||
// inject translations from the current language
|
||||
if ($this->multilang() === true && $language = $this->languages()->find($locale)) {
|
||||
$data = array_merge($data, $language->translations());
|
||||
|
||||
// Add language slug rules to Str class
|
||||
Str::$language = $language->rules();
|
||||
}
|
||||
|
||||
|
||||
return $data;
|
||||
};
|
||||
|
||||
I18n::$locale = function (): string {
|
||||
if ($this->multilang() === true) {
|
||||
return $this->defaultLanguage()->code();
|
||||
} else {
|
||||
return 'en';
|
||||
}
|
||||
};
|
||||
|
||||
I18n::$fallback = function (): string {
|
||||
if ($this->multilang() === true) {
|
||||
return $this->defaultLanguage()->code();
|
||||
} else {
|
||||
return 'en';
|
||||
}
|
||||
};
|
||||
|
||||
I18n::$translations = [];
|
||||
|
||||
if (isset($this->options['slugs']) === true) {
|
||||
$file = $this->root('i18n:rules') . '/' . $this->options['slugs'] . '.json';
|
||||
|
||||
if (F::exists($file) === true) {
|
||||
try {
|
||||
$data = Data::read($file);
|
||||
} catch (\Exception $e) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
Str::$language = $data;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and set the current language if it exists
|
||||
* Otherwise fall back to the default language
|
||||
*
|
||||
* @internal
|
||||
* @param string $languageCode
|
||||
* @return \Kirby\Cms\Language|null
|
||||
*/
|
||||
public function setCurrentLanguage(string $languageCode = null)
|
||||
{
|
||||
if ($this->multilang() === false) {
|
||||
$this->setLocale($this->option('locale', 'en_US.utf-8'));
|
||||
return $this->language = null;
|
||||
}
|
||||
|
||||
if ($language = $this->language($languageCode)) {
|
||||
$this->language = $language;
|
||||
} else {
|
||||
$this->language = $this->defaultLanguage();
|
||||
}
|
||||
|
||||
if ($this->language) {
|
||||
$this->setLocale($this->language->locale());
|
||||
}
|
||||
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current translation
|
||||
*
|
||||
* @internal
|
||||
* @param string $translationCode
|
||||
* @return void
|
||||
*/
|
||||
public function setCurrentTranslation(string $translationCode = null): void
|
||||
{
|
||||
I18n::$locale = $translationCode ?? 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set locale settings
|
||||
*
|
||||
* @internal
|
||||
* @param string|array $locale
|
||||
*/
|
||||
public function setLocale($locale): void
|
||||
{
|
||||
if (is_array($locale) === true) {
|
||||
foreach ($locale as $key => $value) {
|
||||
setlocale($key, $value);
|
||||
}
|
||||
} else {
|
||||
setlocale(LC_ALL, $locale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specific translation by locale
|
||||
*
|
||||
* @param string|null $locale
|
||||
* @return \Kirby\Cms\Translation|null
|
||||
*/
|
||||
public function translation(string $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 ($translation = $this->translations()->find($locale)) {
|
||||
return $translation;
|
||||
}
|
||||
}
|
||||
|
||||
// get injected translation data from plugins etc.
|
||||
$inject = $this->extensions['translations'][$locale] ?? [];
|
||||
|
||||
// load from disk instead
|
||||
return Translation::load($locale, $this->root('i18n:translations') . '/' . $locale . '.json', $inject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available translations
|
||||
*
|
||||
* @return \Kirby\Cms\Translations
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
if (is_a($this->translations, 'Kirby\Cms\Translations') === true) {
|
||||
return $this->translations;
|
||||
}
|
||||
|
||||
return Translations::load($this->root('i18n:translations'), $this->extensions['translations'] ?? []);
|
||||
}
|
||||
}
|
||||
139
kirby/src/Cms/AppUsers.php
Normal file
139
kirby/src/Cms/AppUsers.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* AppUsers
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait AppUsers
|
||||
{
|
||||
/**
|
||||
* Cache for the auth auth layer
|
||||
*
|
||||
* @var Auth
|
||||
*/
|
||||
protected $auth;
|
||||
|
||||
/**
|
||||
* Returns the Authentication layer class
|
||||
*
|
||||
* @internal
|
||||
* @return \Kirby\Cms\Auth
|
||||
*/
|
||||
public function auth()
|
||||
{
|
||||
return $this->auth = $this->auth ?? new Auth($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Become any existing user
|
||||
*
|
||||
* @param string|null $who User ID or email address
|
||||
* @param Closure|null $callback Optional action function that will be run with
|
||||
* the permissions of the impersonated user; the
|
||||
* impersonation will be reset afterwards
|
||||
* @return mixed If called without callback: User that was impersonated;
|
||||
* if called with callback: Return value from the callback
|
||||
*/
|
||||
public function impersonate(?string $who = null, ?Closure $callback = null)
|
||||
{
|
||||
$auth = $this->auth();
|
||||
|
||||
$userBefore = $auth->currentUserFromImpersonation();
|
||||
$userAfter = $auth->impersonate($who);
|
||||
|
||||
if ($callback === null) {
|
||||
return $userAfter;
|
||||
}
|
||||
|
||||
try {
|
||||
// bind the App object to the callback
|
||||
return $callback->call($this, $userAfter);
|
||||
} catch (Throwable $e) {
|
||||
throw $e;
|
||||
} finally {
|
||||
// ensure that the impersonation is *always* reset
|
||||
// to the original value, even if an error occurred
|
||||
$auth->impersonate($userBefore !== null ? $userBefore->id() : null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currently active user id
|
||||
*
|
||||
* @param \Kirby\Cms\User|string $user
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
protected function setUser($user = null)
|
||||
{
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create your own set of app users
|
||||
*
|
||||
* @param array $users
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
protected function setUsers(array $users = null)
|
||||
{
|
||||
if ($users !== null) {
|
||||
$this->users = Users::factory($users, [
|
||||
'kirby' => $this
|
||||
]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific user by id
|
||||
* or the current user if no id is given
|
||||
*
|
||||
* @param string|null $id
|
||||
* @param bool $allowImpersonation If set to false, only the actually
|
||||
* logged in user will be returned
|
||||
* (when `$id` is passed as `null`)
|
||||
* @return \Kirby\Cms\User|null
|
||||
*/
|
||||
public function user(?string $id = null, bool $allowImpersonation = true)
|
||||
{
|
||||
if ($id !== null) {
|
||||
return $this->users()->find($id);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all users
|
||||
*
|
||||
* @return \Kirby\Cms\Users
|
||||
*/
|
||||
public function users()
|
||||
{
|
||||
if (is_a($this->users, 'Kirby\Cms\Users') === true) {
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
return $this->users = Users::load($this->root('accounts'), ['kirby' => $this]);
|
||||
}
|
||||
}
|
||||
126
kirby/src/Cms/Asset.php
Normal file
126
kirby/src/Cms/Asset.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
||||
/**
|
||||
* Anything in your public path can be converted
|
||||
* to an Asset object to use the same handy file
|
||||
* methods and thumbnail generation as for any other
|
||||
* Kirby files. Pass a relative path to the Asset
|
||||
* object to create the asset.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Asset
|
||||
{
|
||||
use FileFoundation;
|
||||
use FileModifications;
|
||||
use Properties;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* Creates a new Asset object
|
||||
* for the given path.
|
||||
*
|
||||
* @param string $path
|
||||
*/
|
||||
public function __construct(string $path)
|
||||
{
|
||||
$this->setPath(dirname($path));
|
||||
$this->setRoot($this->kirby()->root('index') . '/' . $path);
|
||||
$this->setUrl($this->kirby()->url('index') . '/' . $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alternative text for the asset
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
public function alt()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique id for the asset
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->root();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique media hash
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaHash(): string
|
||||
{
|
||||
return crc32($this->filename()) . '-' . $this->modified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the relative path starting at the media folder
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaPath(): string
|
||||
{
|
||||
return 'assets/' . $this->path() . '/' . $this->mediaHash() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file in the public media folder
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return $this->kirby()->root('media') . '/' . $this->mediaPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute Url to the file in the public media folder
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
return $this->kirby()->url('media') . '/' . $this->mediaPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of the file from the web root,
|
||||
* excluding the filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function path(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the path
|
||||
*
|
||||
* @param string $path
|
||||
* @return self
|
||||
*/
|
||||
protected function setPath(string $path)
|
||||
{
|
||||
$this->path = $path === '.' ? '' : $path;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
491
kirby/src/Cms/Auth.php
Normal file
491
kirby/src/Cms/Auth.php
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Http\Request\Auth\BasicAuth;
|
||||
use Kirby\Toolkit\F;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Authentication layer
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
protected $impersonate;
|
||||
protected $kirby;
|
||||
protected $user = false;
|
||||
protected $userException;
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __construct(App $kirby)
|
||||
{
|
||||
$this->kirby = $kirby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the csrf token if it exists and if it is valid
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function csrf()
|
||||
{
|
||||
// get the csrf from the header
|
||||
$fromHeader = $this->kirby->request()->csrf();
|
||||
|
||||
// check for a predefined csrf or use the one from session
|
||||
$fromSession = $this->kirby->option('api.csrf', csrf());
|
||||
|
||||
// compare both tokens
|
||||
if (hash_equals((string)$fromSession, (string)$fromHeader) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $fromSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logged in user by checking
|
||||
* for a basic authentication header with
|
||||
* valid credentials
|
||||
*
|
||||
* @param \Kirby\Http\Request\Auth\BasicAuth|null $auth
|
||||
* @return \Kirby\Cms\User|null
|
||||
*/
|
||||
public function currentUserFromBasicAuth(BasicAuth $auth = null)
|
||||
{
|
||||
if ($this->kirby->option('api.basicAuth', false) !== true) {
|
||||
throw new PermissionException('Basic authentication is not activated');
|
||||
}
|
||||
|
||||
$request = $this->kirby->request();
|
||||
$auth = $auth ?? $request->auth();
|
||||
|
||||
if (!$auth || $auth->type() !== 'basic') {
|
||||
throw new InvalidArgumentException('Invalid authorization header');
|
||||
}
|
||||
|
||||
// only allow basic auth when https is enabled or insecure requests permitted
|
||||
if ($request->ssl() === false && $this->kirby->option('api.allowInsecure', false) !== true) {
|
||||
throw new PermissionException('Basic authentication is only allowed over HTTPS');
|
||||
}
|
||||
|
||||
return $this->validatePassword($auth->username(), $auth->password());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently impersonated user
|
||||
*
|
||||
* @return \Kirby\Cms\User|null
|
||||
*/
|
||||
public function currentUserFromImpersonation()
|
||||
{
|
||||
return $this->impersonate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logged in user by checking
|
||||
* the current session and finding a valid
|
||||
* valid user id in there
|
||||
*
|
||||
* @param \Kirby\Session\Session|array|null $session
|
||||
* @return \Kirby\Cms\User|null
|
||||
*/
|
||||
public function currentUserFromSession($session = null)
|
||||
{
|
||||
// use passed session options or session object if set
|
||||
if (is_array($session) === true) {
|
||||
$session = $this->kirby->session($session);
|
||||
}
|
||||
|
||||
// try session in header or cookie
|
||||
if (is_a($session, 'Kirby\Session\Session') === false) {
|
||||
$session = $this->kirby->session(['detect' => true]);
|
||||
}
|
||||
|
||||
$id = $session->data()->get('user.id');
|
||||
|
||||
if (is_string($id) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($user = $this->kirby->users()->find($id)) {
|
||||
// in case the session needs to be updated, do it now
|
||||
// for better performance
|
||||
$session->commit();
|
||||
return $user;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Become any existing user
|
||||
*
|
||||
* @param string|null $who User ID or email address
|
||||
* @return \Kirby\Cms\User|null
|
||||
*/
|
||||
public function impersonate(?string $who = null)
|
||||
{
|
||||
switch ($who) {
|
||||
case null:
|
||||
return $this->impersonate = null;
|
||||
case 'kirby':
|
||||
return $this->impersonate = new User([
|
||||
'email' => 'kirby@getkirby.com',
|
||||
'id' => 'kirby',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
default:
|
||||
if ($user = $this->kirby->users()->find($who)) {
|
||||
return $this->impersonate = $user;
|
||||
}
|
||||
|
||||
throw new NotFoundException('The user "' . $who . '" cannot be found');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hashed ip of the visitor
|
||||
* which is used to track invalid logins
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function ipHash(): string
|
||||
{
|
||||
$hash = hash('sha256', $this->kirby->visitor()->ip());
|
||||
|
||||
// only use the first 50 chars to ensure privacy
|
||||
return substr($hash, 0, 50);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if logins are blocked for the current ip or email
|
||||
*
|
||||
* @param string $email
|
||||
* @return bool
|
||||
*/
|
||||
public function isBlocked(string $email): bool
|
||||
{
|
||||
$ip = $this->ipHash();
|
||||
$log = $this->log();
|
||||
$trials = $this->kirby->option('auth.trials', 10);
|
||||
|
||||
if ($entry = ($log['by-ip'][$ip] ?? null)) {
|
||||
if ($entry['trials'] >= $trials) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->kirby->users()->find($email)) {
|
||||
if ($entry = ($log['by-email'][$email] ?? null)) {
|
||||
if ($entry['trials'] >= $trials) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login a user by email and password
|
||||
*
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
* @param bool $long
|
||||
* @return \Kirby\Cms\User
|
||||
*
|
||||
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occured with debug mode off
|
||||
* @throws \Kirby\Exception\NotFoundException If the email was invalid
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
|
||||
*/
|
||||
public function login(string $email, string $password, bool $long = false)
|
||||
{
|
||||
// session options
|
||||
$options = [
|
||||
'createMode' => 'cookie',
|
||||
'long' => $long === true
|
||||
];
|
||||
|
||||
// validate the user and log in to the session
|
||||
$user = $this->validatePassword($email, $password);
|
||||
$user->loginPasswordless($options);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a user object as the current user in the cache
|
||||
* @internal
|
||||
*
|
||||
* @param \Kirby\Cms\User $user
|
||||
* @return void
|
||||
*/
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
// stop impersonating
|
||||
$this->impersonate = null;
|
||||
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the user credentials and returns the user object on success;
|
||||
* otherwise logs the failed attempt
|
||||
*
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
* @return \Kirby\Cms\User
|
||||
*
|
||||
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occured with debug mode off
|
||||
* @throws \Kirby\Exception\NotFoundException If the email was invalid
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
|
||||
*/
|
||||
public function validatePassword(string $email, string $password)
|
||||
{
|
||||
// check for blocked ips
|
||||
if ($this->isBlocked($email) === true) {
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
$message = 'Rate limit exceeded';
|
||||
} else {
|
||||
// avoid leaking security-relevant information
|
||||
$message = 'Invalid email or password';
|
||||
}
|
||||
|
||||
throw new PermissionException($message);
|
||||
}
|
||||
|
||||
// validate the user
|
||||
try {
|
||||
if ($user = $this->kirby->users()->find($email)) {
|
||||
if ($user->validatePassword($password) === true) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $email
|
||||
]
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
// log invalid login trial
|
||||
$this->track($email);
|
||||
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder
|
||||
usleep(random_int(1000, 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('Invalid email or password');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the logins log
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function logfile(): string
|
||||
{
|
||||
return $this->kirby->root('accounts') . '/.logins';
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all tracked logins
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function log(): array
|
||||
{
|
||||
try {
|
||||
$log = Data::read($this->logfile(), 'json');
|
||||
$read = true;
|
||||
} catch (Throwable $e) {
|
||||
$log = [];
|
||||
$read = false;
|
||||
}
|
||||
|
||||
// ensure that the category arrays are defined
|
||||
$log['by-ip'] = $log['by-ip'] ?? [];
|
||||
$log['by-email'] = $log['by-email'] ?? [];
|
||||
|
||||
// remove all elements on the top level with different keys (old structure)
|
||||
$log = array_intersect_key($log, array_flip(['by-ip', 'by-email']));
|
||||
|
||||
// remove entries that are no longer needed
|
||||
$originalLog = $log;
|
||||
$time = time() - $this->kirby->option('auth.timeout', 3600);
|
||||
foreach ($log as $category => $entries) {
|
||||
$log[$category] = array_filter($entries, function ($entry) use ($time) {
|
||||
return $entry['time'] > $time;
|
||||
});
|
||||
}
|
||||
|
||||
// write new log to the file system if it changed
|
||||
if ($read === false || $log !== $originalLog) {
|
||||
if (count($log['by-ip']) === 0 && count($log['by-email']) === 0) {
|
||||
F::remove($this->logfile());
|
||||
} else {
|
||||
Data::write($this->logfile(), $log, 'json');
|
||||
}
|
||||
}
|
||||
|
||||
return $log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the current user
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function logout(): void
|
||||
{
|
||||
// stop impersonating;
|
||||
// ensures that we log out the actually logged in user
|
||||
$this->impersonate = null;
|
||||
|
||||
// logout the current user if it exists
|
||||
if ($user = $this->user()) {
|
||||
$user->logout();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the cached user data after logout
|
||||
* @internal
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function flush(): void
|
||||
{
|
||||
$this->impersonate = null;
|
||||
$this->user = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks a login
|
||||
*
|
||||
* @param string $email
|
||||
* @return bool
|
||||
*/
|
||||
public function track(string $email): bool
|
||||
{
|
||||
$ip = $this->ipHash();
|
||||
$log = $this->log();
|
||||
$time = time();
|
||||
|
||||
if (isset($log['by-ip'][$ip]) === true) {
|
||||
$log['by-ip'][$ip] = [
|
||||
'time' => $time,
|
||||
'trials' => ($log['by-ip'][$ip]['trials'] ?? 0) + 1
|
||||
];
|
||||
} else {
|
||||
$log['by-ip'][$ip] = [
|
||||
'time' => $time,
|
||||
'trials' => 1
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->kirby->users()->find($email)) {
|
||||
if (isset($log['by-email'][$email]) === true) {
|
||||
$log['by-email'][$email] = [
|
||||
'time' => $time,
|
||||
'trials' => ($log['by-email'][$email]['trials'] ?? 0) + 1
|
||||
];
|
||||
} else {
|
||||
$log['by-email'][$email] = [
|
||||
'time' => $time,
|
||||
'trials' => 1
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return Data::write($this->logfile(), $log, 'json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current authentication type
|
||||
*
|
||||
* @param bool $allowImpersonation If set to false, 'impersonate' won't
|
||||
* be returned as authentication type
|
||||
* even if an impersonation is active
|
||||
* @return string
|
||||
*/
|
||||
public function type(bool $allowImpersonation = true): string
|
||||
{
|
||||
$basicAuth = $this->kirby->option('api.basicAuth', false);
|
||||
$auth = $this->kirby->request()->auth();
|
||||
|
||||
if ($basicAuth === true && $auth && $auth->type() === 'basic') {
|
||||
return 'basic';
|
||||
} elseif ($allowImpersonation === true && $this->impersonate !== null) {
|
||||
return 'impersonate';
|
||||
} else {
|
||||
return 'session';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the currently logged in user
|
||||
*
|
||||
* @param \Kirby\Session\Session|array|null $session
|
||||
* @param bool $allowImpersonation If set to false, only the actually
|
||||
* logged in user will be returned
|
||||
* @return \Kirby\Cms\User
|
||||
*
|
||||
* @throws \Throwable If an authentication error occured
|
||||
*/
|
||||
public function user($session = null, bool $allowImpersonation = true)
|
||||
{
|
||||
if ($allowImpersonation === true && $this->impersonate !== null) {
|
||||
return $this->impersonate;
|
||||
}
|
||||
|
||||
// return from cache
|
||||
if ($this->user === null) {
|
||||
// throw the same Exception again if one was captured before
|
||||
if ($this->userException !== null) {
|
||||
throw $this->userException;
|
||||
}
|
||||
|
||||
return null;
|
||||
} elseif ($this->user !== false) {
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->type() === 'basic') {
|
||||
return $this->user = $this->currentUserFromBasicAuth();
|
||||
} else {
|
||||
return $this->user = $this->currentUserFromSession($session);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->user = null;
|
||||
|
||||
// capture the Exception for future calls
|
||||
$this->userException = $e;
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
790
kirby/src/Cms/Blueprint.php
Normal file
790
kirby/src/Cms/Blueprint.php
Normal file
|
|
@ -0,0 +1,790 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Form\Field;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The Blueprint class normalizes an array from a
|
||||
* blueprint file and converts sections, columns, fields
|
||||
* etc. into a correct tab layout.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Blueprint
|
||||
{
|
||||
public static $presets = [];
|
||||
public static $loaded = [];
|
||||
|
||||
protected $fields = [];
|
||||
protected $model;
|
||||
protected $props;
|
||||
protected $sections = [];
|
||||
protected $tabs = [];
|
||||
|
||||
/**
|
||||
* Magic getter/caller for any blueprint prop
|
||||
*
|
||||
* @param string $key
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $key, array $arguments = null)
|
||||
{
|
||||
return $this->props[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new blueprint object with the given props
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
if (empty($props['model']) === true) {
|
||||
throw new InvalidArgumentException('A blueprint model is required');
|
||||
}
|
||||
|
||||
$this->model = $props['model'];
|
||||
|
||||
// the model should not be included in the props array
|
||||
unset($props['model']);
|
||||
|
||||
// extend the blueprint in general
|
||||
$props = $this->extend($props);
|
||||
|
||||
// apply any blueprint preset
|
||||
$props = $this->preset($props);
|
||||
|
||||
// normalize the name
|
||||
$props['name'] = $props['name'] ?? 'default';
|
||||
|
||||
// normalize and translate the title
|
||||
$props['title'] = $this->i18n($props['title'] ?? ucfirst($props['name']));
|
||||
|
||||
// convert all shortcuts
|
||||
$props = $this->convertFieldsToSections('main', $props);
|
||||
$props = $this->convertSectionsToColumns('main', $props);
|
||||
$props = $this->convertColumnsToTabs('main', $props);
|
||||
|
||||
// normalize all tabs
|
||||
$props['tabs'] = $this->normalizeTabs($props['tabs'] ?? []);
|
||||
|
||||
$this->props = $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->props ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all column definitions, that
|
||||
* are not wrapped in a tab, into a generic tab
|
||||
*
|
||||
* @param string $tabName
|
||||
* @param array $props
|
||||
* @return array
|
||||
*/
|
||||
protected function convertColumnsToTabs(string $tabName, array $props): array
|
||||
{
|
||||
if (isset($props['columns']) === false) {
|
||||
return $props;
|
||||
}
|
||||
|
||||
// wrap everything in a main tab
|
||||
$props['tabs'] = [
|
||||
$tabName => [
|
||||
'columns' => $props['columns']
|
||||
]
|
||||
];
|
||||
|
||||
unset($props['columns']);
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all field definitions, that are not
|
||||
* wrapped in a fields section into a generic
|
||||
* fields section.
|
||||
*
|
||||
* @param string $tabName
|
||||
* @param array $props
|
||||
* @return array
|
||||
*/
|
||||
protected function convertFieldsToSections(string $tabName, array $props): array
|
||||
{
|
||||
if (isset($props['fields']) === false) {
|
||||
return $props;
|
||||
}
|
||||
|
||||
// wrap all fields in a section
|
||||
$props['sections'] = [
|
||||
$tabName . '-fields' => [
|
||||
'type' => 'fields',
|
||||
'fields' => $props['fields']
|
||||
]
|
||||
];
|
||||
|
||||
unset($props['fields']);
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all sections that are not wrapped in
|
||||
* columns, into a single generic column.
|
||||
*
|
||||
* @param string $tabName
|
||||
* @param array $props
|
||||
* @return array
|
||||
*/
|
||||
protected function convertSectionsToColumns(string $tabName, array $props): array
|
||||
{
|
||||
if (isset($props['sections']) === false) {
|
||||
return $props;
|
||||
}
|
||||
|
||||
// wrap everything in one big column
|
||||
$props['columns'] = [
|
||||
[
|
||||
'width' => '1/1',
|
||||
'sections' => $props['sections']
|
||||
]
|
||||
];
|
||||
|
||||
unset($props['sections']);
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the props with props from a given
|
||||
* mixin, when an extends key is set or the
|
||||
* props is just a string
|
||||
*
|
||||
* @param array|string $props
|
||||
* @return array
|
||||
*/
|
||||
public static function extend($props): array
|
||||
{
|
||||
if (is_string($props) === true) {
|
||||
$props = [
|
||||
'extends' => $props
|
||||
];
|
||||
}
|
||||
|
||||
$extends = $props['extends'] ?? null;
|
||||
|
||||
if ($extends === null) {
|
||||
return $props;
|
||||
}
|
||||
|
||||
try {
|
||||
$mixin = static::find($extends);
|
||||
$props = A::merge($mixin, $props, A::MERGE_REPLACE);
|
||||
} catch (Exception $e) {
|
||||
// keep the props unextended if the snippet wasn't found
|
||||
}
|
||||
|
||||
// remove the extends flag
|
||||
unset($props['extends']);
|
||||
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new blueprint for a model
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $fallback
|
||||
* @param \Kirby\Cms\Model $model
|
||||
* @return self
|
||||
*/
|
||||
public static function factory(string $name, string $fallback = null, Model $model)
|
||||
{
|
||||
try {
|
||||
$props = static::load($name);
|
||||
} catch (Exception $e) {
|
||||
$props = $fallback !== null ? static::load($fallback) : null;
|
||||
}
|
||||
|
||||
if ($props === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// inject the parent model
|
||||
$props['model'] = $model;
|
||||
|
||||
return new static($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single field definition by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function field(string $name): ?array
|
||||
{
|
||||
return $this->fields[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all field definitions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a blueprint by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return array
|
||||
*/
|
||||
public static function find(string $name): array
|
||||
{
|
||||
if (isset(static::$loaded[$name]) === true) {
|
||||
return static::$loaded[$name];
|
||||
}
|
||||
|
||||
$kirby = App::instance();
|
||||
$root = $kirby->root('blueprints');
|
||||
$file = $root . '/' . $name . '.yml';
|
||||
|
||||
// first try to find a site blueprint,
|
||||
// then check in the plugin extensions
|
||||
if (F::exists($file, $root) !== true) {
|
||||
$file = $kirby->extension('blueprints', $name);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return static::$loaded[$name] = $file;
|
||||
}
|
||||
|
||||
// neither a valid file nor array data
|
||||
throw new NotFoundException([
|
||||
'key' => 'blueprint.notFound',
|
||||
'data' => ['name' => $name]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to translate any label, heading, etc.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param mixed $fallback
|
||||
* @return mixed
|
||||
*/
|
||||
protected function i18n($value, $fallback = null)
|
||||
{
|
||||
return I18n::translate($value, $fallback ?? $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is the default blueprint
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefault(): bool
|
||||
{
|
||||
return $this->name() === 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a blueprint from file or array
|
||||
*
|
||||
* @param string $name
|
||||
* @return array
|
||||
*/
|
||||
public static function load(string $name): array
|
||||
{
|
||||
$props = static::find($name);
|
||||
|
||||
$normalize = function ($props) use ($name) {
|
||||
// inject the filename as name if no name is set
|
||||
$props['name'] = $props['name'] ?? $name;
|
||||
|
||||
// normalize the title
|
||||
$title = $props['title'] ?? ucfirst($props['name']);
|
||||
|
||||
// translate the title
|
||||
$props['title'] = I18n::translate($title, $title);
|
||||
|
||||
return $props;
|
||||
};
|
||||
|
||||
return $normalize($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model
|
||||
*
|
||||
* @return \Kirby\Cms\Model
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the blueprint name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->props['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes all required props in a column setup
|
||||
*
|
||||
* @param string $tabName
|
||||
* @param array $columns
|
||||
* @return array
|
||||
*/
|
||||
protected function normalizeColumns(string $tabName, array $columns): array
|
||||
{
|
||||
foreach ($columns as $columnKey => $columnProps) {
|
||||
if (is_array($columnProps) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columnProps = $this->convertFieldsToSections($tabName . '-col-' . $columnKey, $columnProps);
|
||||
|
||||
// inject getting started info, if the sections are empty
|
||||
if (empty($columnProps['sections']) === true) {
|
||||
$columnProps['sections'] = [
|
||||
$tabName . '-info-' . $columnKey => [
|
||||
'headline' => 'Column (' . ($columnProps['width'] ?? '1/1') . ')',
|
||||
'type' => 'info',
|
||||
'text' => 'No sections yet'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$columns[$columnKey] = array_merge($columnProps, [
|
||||
'width' => $columnProps['width'] ?? '1/1',
|
||||
'sections' => $this->normalizeSections($tabName, $columnProps['sections'] ?? [])
|
||||
]);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public static function helpList(array $items): string
|
||||
{
|
||||
$md = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
$md[] = '- *' . $item . '*';
|
||||
}
|
||||
|
||||
return PHP_EOL . implode(PHP_EOL, $md);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize field props for a single field
|
||||
*
|
||||
* @param array|string $props
|
||||
* @return array
|
||||
*/
|
||||
public static function fieldProps($props): array
|
||||
{
|
||||
$props = static::extend($props);
|
||||
|
||||
if (isset($props['name']) === false) {
|
||||
throw new InvalidArgumentException('The field name is missing');
|
||||
}
|
||||
|
||||
$name = $props['name'];
|
||||
$type = $props['type'] ?? $name;
|
||||
|
||||
if ($type !== 'group' && isset(Field::$types[$type]) === false) {
|
||||
throw new InvalidArgumentException('Invalid field type ("' . $type . '")');
|
||||
}
|
||||
|
||||
// support for nested fields
|
||||
if (isset($props['fields']) === true) {
|
||||
$props['fields'] = static::fieldsProps($props['fields']);
|
||||
}
|
||||
|
||||
// groups don't need all the crap
|
||||
if ($type === 'group') {
|
||||
return [
|
||||
'fields' => $props['fields'],
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
];
|
||||
}
|
||||
|
||||
// add some useful defaults
|
||||
return array_merge($props, [
|
||||
'label' => $props['label'] ?? ucfirst($name),
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'width' => $props['width'] ?? '1/1',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error field with the given error message
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $message
|
||||
* @return array
|
||||
*/
|
||||
public static function fieldError(string $name, string $message): array
|
||||
{
|
||||
return [
|
||||
'label' => 'Error',
|
||||
'name' => $name,
|
||||
'text' => strip_tags($message),
|
||||
'theme' => 'negative',
|
||||
'type' => 'info',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes all fields and adds automatic labels,
|
||||
* types and widths.
|
||||
*
|
||||
* @param array $fields
|
||||
* @return array
|
||||
*/
|
||||
public static function fieldsProps($fields): array
|
||||
{
|
||||
if (is_array($fields) === false) {
|
||||
$fields = [];
|
||||
}
|
||||
|
||||
foreach ($fields as $fieldName => $fieldProps) {
|
||||
|
||||
// extend field from string
|
||||
if (is_string($fieldProps) === true) {
|
||||
$fieldProps = [
|
||||
'extends' => $fieldProps,
|
||||
'name' => $fieldName
|
||||
];
|
||||
}
|
||||
|
||||
// use the name as type definition
|
||||
if ($fieldProps === true) {
|
||||
$fieldProps = [];
|
||||
}
|
||||
|
||||
// unset / remove field if its propperty is false
|
||||
if ($fieldProps === false) {
|
||||
unset($fields[$fieldName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// inject the name
|
||||
$fieldProps['name'] = $fieldName;
|
||||
|
||||
// create all props
|
||||
try {
|
||||
$fieldProps = static::fieldProps($fieldProps);
|
||||
} catch (Throwable $e) {
|
||||
$fieldProps = static::fieldError($fieldName, $e->getMessage());
|
||||
}
|
||||
|
||||
// resolve field groups
|
||||
if ($fieldProps['type'] === 'group') {
|
||||
if (empty($fieldProps['fields']) === false && is_array($fieldProps['fields']) === true) {
|
||||
$index = array_search($fieldName, array_keys($fields));
|
||||
$before = array_slice($fields, 0, $index);
|
||||
$after = array_slice($fields, $index + 1);
|
||||
$fields = array_merge($before, $fieldProps['fields'] ?? [], $after);
|
||||
} else {
|
||||
unset($fields[$fieldName]);
|
||||
}
|
||||
} else {
|
||||
$fields[$fieldName] = $fieldProps;
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes blueprint options. This must be used in the
|
||||
* constructor of an extended class, if you want to make use of it.
|
||||
*
|
||||
* @param array|true|false|null|string $options
|
||||
* @param array $defaults
|
||||
* @param array $aliases
|
||||
* @return array
|
||||
*/
|
||||
protected function normalizeOptions($options, array $defaults, array $aliases = []): array
|
||||
{
|
||||
// return defaults when options are not defined or set to true
|
||||
if ($options === true) {
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
// set all options to false
|
||||
if ($options === false) {
|
||||
return array_map(function () {
|
||||
return false;
|
||||
}, $defaults);
|
||||
}
|
||||
|
||||
// extend options if possible
|
||||
$options = $this->extend($options);
|
||||
|
||||
foreach ($options as $key => $value) {
|
||||
$alias = $aliases[$key] ?? null;
|
||||
|
||||
if ($alias !== null) {
|
||||
$options[$alias] = $options[$alias] ?? $value;
|
||||
unset($options[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($defaults, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes all required keys in sections
|
||||
*
|
||||
* @param string $tabName
|
||||
* @param array $sections
|
||||
* @return array
|
||||
*/
|
||||
protected function normalizeSections(string $tabName, array $sections): array
|
||||
{
|
||||
foreach ($sections as $sectionName => $sectionProps) {
|
||||
|
||||
// unset / remove section if its propperty is false
|
||||
if ($sectionProps === false) {
|
||||
unset($sections[$sectionName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// fallback to default props when true is passed
|
||||
if ($sectionProps === true) {
|
||||
$sectionProps = [];
|
||||
}
|
||||
|
||||
// inject all section extensions
|
||||
$sectionProps = $this->extend($sectionProps);
|
||||
|
||||
$sections[$sectionName] = $sectionProps = array_merge($sectionProps, [
|
||||
'name' => $sectionName,
|
||||
'type' => $type = $sectionProps['type'] ?? $sectionName
|
||||
]);
|
||||
|
||||
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))
|
||||
];
|
||||
} 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))
|
||||
];
|
||||
}
|
||||
|
||||
if ($sectionProps['type'] === 'fields') {
|
||||
$fields = Blueprint::fieldsProps($sectionProps['fields'] ?? []);
|
||||
|
||||
// inject guide fields guide
|
||||
if (empty($fields) === true) {
|
||||
$fields = [
|
||||
$tabName . '-info' => [
|
||||
'label' => 'Fields',
|
||||
'text' => 'No fields yet',
|
||||
'type' => 'info'
|
||||
]
|
||||
];
|
||||
} else {
|
||||
foreach ($fields as $fieldName => $fieldProps) {
|
||||
if (isset($this->fields[$fieldName]) === true) {
|
||||
$this->fields[$fieldName] = $fields[$fieldName] = [
|
||||
'type' => 'info',
|
||||
'label' => $fieldProps['label'] ?? 'Error',
|
||||
'text' => 'The field name <strong>"' . $fieldName . '"</strong> already exists in your blueprint.',
|
||||
'theme' => 'negative'
|
||||
];
|
||||
} else {
|
||||
$this->fields[$fieldName] = $fieldProps;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sections[$sectionName]['fields'] = $fields;
|
||||
}
|
||||
}
|
||||
|
||||
// store all normalized sections
|
||||
$this->sections = array_merge($this->sections, $sections);
|
||||
|
||||
return $sections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes all required keys in tabs
|
||||
*
|
||||
* @param array $tabs
|
||||
* @return array
|
||||
*/
|
||||
protected function normalizeTabs($tabs): array
|
||||
{
|
||||
if (is_array($tabs) === false) {
|
||||
$tabs = [];
|
||||
}
|
||||
|
||||
foreach ($tabs as $tabName => $tabProps) {
|
||||
|
||||
// unset / remove tab if its propperty is false
|
||||
if ($tabProps === false) {
|
||||
unset($tabs[$tabName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// inject all tab extensions
|
||||
$tabProps = $this->extend($tabProps);
|
||||
|
||||
// inject a preset if available
|
||||
$tabProps = $this->preset($tabProps);
|
||||
|
||||
$tabProps = $this->convertFieldsToSections($tabName, $tabProps);
|
||||
$tabProps = $this->convertSectionsToColumns($tabName, $tabProps);
|
||||
|
||||
$tabs[$tabName] = array_merge($tabProps, [
|
||||
'columns' => $this->normalizeColumns($tabName, $tabProps['columns'] ?? []),
|
||||
'icon' => $tabProps['icon'] ?? null,
|
||||
'label' => $this->i18n($tabProps['label'] ?? ucfirst($tabName)),
|
||||
'name' => $tabName,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->tabs = $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects a blueprint preset
|
||||
*
|
||||
* @param array $props
|
||||
* @return array
|
||||
*/
|
||||
protected function preset(array $props): array
|
||||
{
|
||||
if (isset($props['preset']) === false) {
|
||||
return $props;
|
||||
}
|
||||
|
||||
if (isset(static::$presets[$props['preset']]) === false) {
|
||||
return $props;
|
||||
}
|
||||
|
||||
return static::$presets[$props['preset']]($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single section by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return \Kirby\Cms\Section|null
|
||||
*/
|
||||
public function section(string $name)
|
||||
{
|
||||
if (empty($this->sections[$name]) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// get all props
|
||||
$props = $this->sections[$name];
|
||||
|
||||
// inject the blueprint model
|
||||
$props['model'] = $this->model();
|
||||
|
||||
// create a new section object
|
||||
return new Section($props['type'], $props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all sections
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sections(): array
|
||||
{
|
||||
return array_map(function ($section) {
|
||||
return $this->section($section['name']);
|
||||
}, $this->sections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single tab by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return array|null
|
||||
*/
|
||||
public function tab(string $name): ?array
|
||||
{
|
||||
return $this->tabs[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all tabs
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function tabs(): array
|
||||
{
|
||||
return array_values($this->tabs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the blueprint title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function title(): string
|
||||
{
|
||||
return $this->props['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the blueprint object to a plain array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->props;
|
||||
}
|
||||
}
|
||||
333
kirby/src/Cms/Collection.php
Normal file
333
kirby/src/Cms/Collection.php
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Collection as BaseCollection;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Collection class serves as foundation
|
||||
* for the Pages, Files, Users and Structure
|
||||
* classes. It handles object validation and sets
|
||||
* the parent collection property for each object.
|
||||
* The `getAttribute` method is also adjusted to
|
||||
* handle values from Field objects correctly, so
|
||||
* those can be used in filters as well.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Collection extends BaseCollection
|
||||
{
|
||||
use HasMethods;
|
||||
|
||||
/**
|
||||
* Stores the parent object, which is needed
|
||||
* in some collections to get the finder methods right.
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* Magic getter function
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $key, $arguments)
|
||||
{
|
||||
// collection methods
|
||||
if ($this->hasMethod($key) === true) {
|
||||
return $this->callMethod($key, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Collection with the given objects
|
||||
*
|
||||
* @param array $objects
|
||||
* @param object $parent
|
||||
*/
|
||||
public function __construct($objects = [], $parent = null)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
|
||||
foreach ($objects as $object) {
|
||||
$this->add($object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal setter for each object in the Collection.
|
||||
* This takes care of Component validation and of setting
|
||||
* the collection prop on each object correctly.
|
||||
*
|
||||
* @param string $id
|
||||
* @param object $object
|
||||
*/
|
||||
public function __set(string $id, $object)
|
||||
{
|
||||
$this->data[$id] = $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single object or
|
||||
* an entire second collection to the
|
||||
* current collection
|
||||
*
|
||||
* @param mixed $object
|
||||
*/
|
||||
public function add($object)
|
||||
{
|
||||
if (is_a($object, static::class) === true) {
|
||||
$this->data = array_merge($this->data, $object->data);
|
||||
} elseif (method_exists($object, 'id') === true) {
|
||||
$this->__set($object->id(), $object);
|
||||
} else {
|
||||
$this->append($object);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an element to the data array
|
||||
*
|
||||
* @param mixed $key Optional collection key, will be determined from the item if not given
|
||||
* @param mixed $item
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function append(...$args)
|
||||
{
|
||||
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) {
|
||||
return parent::append($args[0]->id(), $args[0]);
|
||||
} else {
|
||||
return parent::append($args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::append(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the items by a given field. Returns a collection
|
||||
* with an item for each group and a collection for each group.
|
||||
*
|
||||
* @param string $field
|
||||
* @param bool $i Ignore upper/lowercase for group names
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function groupBy($field, bool $i = true)
|
||||
{
|
||||
if (is_string($field) === false) {
|
||||
throw new Exception('Cannot group by non-string values. Did you mean to call group()?');
|
||||
}
|
||||
|
||||
$groups = new Collection([], $this->parent());
|
||||
|
||||
foreach ($this->data as $key => $item) {
|
||||
$value = $this->getAttribute($item, $field);
|
||||
|
||||
// make sure that there's always a proper value to group by
|
||||
if (!$value) {
|
||||
throw new InvalidArgumentException('Invalid grouping value for key: ' . $key);
|
||||
}
|
||||
|
||||
// ignore upper/lowercase for group names
|
||||
if ($i) {
|
||||
$value = Str::lower($value);
|
||||
}
|
||||
|
||||
if (isset($groups->data[$value]) === false) {
|
||||
// create a new entry for the group if it does not exist yet
|
||||
$groups->data[$value] = new static([$key => $item]);
|
||||
} else {
|
||||
// add the item to an existing group
|
||||
$groups->data[$value]->set($key, $item);
|
||||
}
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given object or id
|
||||
* is in the collection
|
||||
*
|
||||
* @param string|object $id
|
||||
* @return bool
|
||||
*/
|
||||
public function has($id): bool
|
||||
{
|
||||
if (is_object($id) === true) {
|
||||
$id = $id->id();
|
||||
}
|
||||
|
||||
return parent::has($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct position detection for objects.
|
||||
* The method will automatically detect objects
|
||||
* or ids and then search accordingly.
|
||||
*
|
||||
* @param string|object $object
|
||||
* @return int
|
||||
*/
|
||||
public function indexOf($object): int
|
||||
{
|
||||
if (is_string($object) === true) {
|
||||
return array_search($object, $this->keys());
|
||||
}
|
||||
|
||||
return array_search($object->id(), $this->keys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Collection without the given element(s)
|
||||
*
|
||||
* @param mixed ...$keys any number of keys, passed as individual arguments
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function not(...$keys)
|
||||
{
|
||||
$collection = $this->clone();
|
||||
foreach ($keys as $key) {
|
||||
if (is_a($key, 'Kirby\Toolkit\Collection') === true) {
|
||||
$collection = $collection->not(...$key->keys());
|
||||
} elseif (is_object($key) === true) {
|
||||
$key = $key->id();
|
||||
}
|
||||
unset($collection->$key);
|
||||
}
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add pagination and return a sliced set of data.
|
||||
*
|
||||
* @param mixed ...$arguments
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function paginate(...$arguments)
|
||||
{
|
||||
$this->pagination = Pagination::for($this, ...$arguments);
|
||||
|
||||
// slice and clone the collection according to the pagination
|
||||
return $this->slice($this->pagination->offset(), $this->pagination->limit());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model
|
||||
*
|
||||
* @return \Kirby\Cms\Model
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends an element to the data array
|
||||
*
|
||||
* @param mixed $key Optional collection key, will be determined from the item if not given
|
||||
* @param mixed $item
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function prepend(...$args)
|
||||
{
|
||||
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) {
|
||||
return parent::prepend($args[0]->id(), $args[0]);
|
||||
} else {
|
||||
return parent::prepend($args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return parent::prepend(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a combination of filterBy, sortBy, not
|
||||
* offset, limit, search and paginate on the collection.
|
||||
* Any part of the query is optional.
|
||||
*
|
||||
* @param array $query
|
||||
* @return self
|
||||
*/
|
||||
public function query(array $query = [])
|
||||
{
|
||||
$paginate = $query['paginate'] ?? null;
|
||||
$search = $query['search'] ?? null;
|
||||
|
||||
unset($query['paginate']);
|
||||
|
||||
$result = parent::query($query);
|
||||
|
||||
if (empty($search) === false) {
|
||||
if (is_array($search) === true) {
|
||||
$result = $result->search($search['query'] ?? null, $search['options'] ?? []);
|
||||
} else {
|
||||
$result = $result->search($search);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($paginate) === false) {
|
||||
$result = $result->paginate($paginate);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an object
|
||||
*
|
||||
* @param mixed $key the name of the key
|
||||
*/
|
||||
public function remove($key)
|
||||
{
|
||||
if (is_object($key) === true) {
|
||||
$key = $key->id();
|
||||
}
|
||||
|
||||
return parent::remove($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the collection
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $params
|
||||
* @return self
|
||||
*/
|
||||
public function search(string $query = null, $params = [])
|
||||
{
|
||||
return Search::collection($this, $query, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all objects in the collection
|
||||
* to an array. This can also take a callback
|
||||
* function to further modify the array result.
|
||||
*
|
||||
* @param Closure $map
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Closure $map = null): array
|
||||
{
|
||||
return parent::toArray($map ?? function ($object) {
|
||||
return $object->toArray();
|
||||
});
|
||||
}
|
||||
}
|
||||
140
kirby/src/Cms/Collections.php
Normal file
140
kirby/src/Cms/Collections.php
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Toolkit\Controller;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* Manages and loads all collections
|
||||
* in site/collections, which can then
|
||||
* be reused in controllers, templates, etc
|
||||
*
|
||||
* This class is mainly used in the `$kirby->collection()`
|
||||
* method to provide easy access to registered collections
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Collections
|
||||
{
|
||||
/**
|
||||
* Each collection is cached once it
|
||||
* has been called, to avoid further
|
||||
* processing on sequential calls to
|
||||
* the same collection.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cache = [];
|
||||
|
||||
/**
|
||||
* Store of all collections
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $collections = [];
|
||||
|
||||
/**
|
||||
* Magic caller to enable something like
|
||||
* `$collections->myCollection()`
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return \Kirby\Cms\Collection|null
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
return $this->get($name, ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a collection by name if registered
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
* @return \Kirby\Cms\Collection|null
|
||||
*/
|
||||
public function get(string $name, array $data = [])
|
||||
{
|
||||
// if not yet loaded
|
||||
if (isset($this->collections[$name]) === false) {
|
||||
$this->collections[$name] = $this->load($name);
|
||||
}
|
||||
|
||||
// if not yet cached
|
||||
if (
|
||||
isset($this->cache[$name]) === false ||
|
||||
$this->cache[$name]['data'] !== $data
|
||||
) {
|
||||
$controller = new Controller($this->collections[$name]);
|
||||
$this->cache[$name] = [
|
||||
'result' => $controller->call(null, $data),
|
||||
'data' => $data
|
||||
];
|
||||
}
|
||||
|
||||
// return cloned object
|
||||
if (is_object($this->cache[$name]['result']) === true) {
|
||||
return clone $this->cache[$name]['result'];
|
||||
}
|
||||
|
||||
return $this->cache[$name]['result'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a collection exists
|
||||
*
|
||||
* @param string $name
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
if (isset($this->collections[$name]) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->load($name);
|
||||
return true;
|
||||
} catch (NotFoundException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads collection from php file in a
|
||||
* given directory or from plugin extension.
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function load(string $name)
|
||||
{
|
||||
$kirby = App::instance();
|
||||
|
||||
// first check for collection file
|
||||
$file = $kirby->root('collections') . '/' . $name . '.php';
|
||||
|
||||
if (is_file($file) === true) {
|
||||
$collection = F::load($file);
|
||||
|
||||
if (is_a($collection, 'Closure')) {
|
||||
return $collection;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to collections from plugins
|
||||
$collections = $kirby->extensions('collections');
|
||||
|
||||
if (isset($collections[$name]) === true) {
|
||||
return $collections[$name];
|
||||
}
|
||||
|
||||
throw new NotFoundException('The collection cannot be found');
|
||||
}
|
||||
}
|
||||
266
kirby/src/Cms/Content.php
Normal file
266
kirby/src/Cms/Content.php
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The Content class handles all fields
|
||||
* for content from pages, the site and users
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Content
|
||||
{
|
||||
/**
|
||||
* The raw data array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Cached field objects
|
||||
* Once a field is being fetched
|
||||
* it is added to this array for
|
||||
* later reuse
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = [];
|
||||
|
||||
/**
|
||||
* A potential parent object.
|
||||
* Not necessarily needed. Especially
|
||||
* for testing, but field methods might
|
||||
* need it.
|
||||
*
|
||||
* @var Model
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* Magic getter for content fields
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return \Kirby\Cms\Field
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
return $this->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Content object
|
||||
*
|
||||
* @param array|null $data
|
||||
* @param object|null $parent
|
||||
*/
|
||||
public function __construct(array $data = [], $parent = null)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `self::data()` to improve
|
||||
* `var_dump` output
|
||||
*
|
||||
* @see self::data()
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the content to a new blueprint
|
||||
*
|
||||
* @param string $to
|
||||
* @return array
|
||||
*/
|
||||
public function convertTo(string $to): array
|
||||
{
|
||||
// prepare data
|
||||
$data = [];
|
||||
$content = $this;
|
||||
|
||||
// blueprints
|
||||
$old = $this->parent->blueprint();
|
||||
$subfolder = dirname($old->name());
|
||||
$new = Blueprint::factory($subfolder . '/' . $to, $subfolder . '/default', $this->parent);
|
||||
|
||||
// forms
|
||||
$oldForm = new Form(['fields' => $old->fields(), 'model' => $this->parent]);
|
||||
$newForm = new Form(['fields' => $new->fields(), 'model' => $this->parent]);
|
||||
|
||||
// fields
|
||||
$oldFields = $oldForm->fields();
|
||||
$newFields = $newForm->fields();
|
||||
|
||||
// go through all fields of new template
|
||||
foreach ($newFields as $newField) {
|
||||
$name = $newField->name();
|
||||
$oldField = $oldFields->get($name);
|
||||
|
||||
// field name and type matches with old template
|
||||
if ($oldField && $oldField->type() === $newField->type()) {
|
||||
$data[$name] = $content->get($name)->value();
|
||||
} else {
|
||||
$data[$name] = $newField->default();
|
||||
}
|
||||
}
|
||||
|
||||
// preserve existing fields
|
||||
return array_merge($this->data, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all registered field objects
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fields(): array
|
||||
{
|
||||
foreach ($this->data as $key => $value) {
|
||||
$this->get($key);
|
||||
}
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either a single field object
|
||||
* or all registered fields
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cms\Field|array
|
||||
*/
|
||||
public function get(string $key = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return $this->fields();
|
||||
}
|
||||
|
||||
$key = strtolower($key);
|
||||
|
||||
if (isset($this->fields[$key])) {
|
||||
return $this->fields[$key];
|
||||
}
|
||||
|
||||
// fetch the value no matter the case
|
||||
$data = $this->data();
|
||||
$value = $data[$key] ?? array_change_key_case($data)[$key] ?? null;
|
||||
|
||||
return $this->fields[$key] = new Field($this->parent, $key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a content field is set
|
||||
*
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
$key = strtolower($key);
|
||||
$data = array_change_key_case($this->data);
|
||||
|
||||
return isset($data[$key]) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all field keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
return array_keys($this->data());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clone of the content object
|
||||
* without the fields, specified by the
|
||||
* passed key(s)
|
||||
*
|
||||
* @param string ...$keys
|
||||
* @return self
|
||||
*/
|
||||
public function not(...$keys)
|
||||
{
|
||||
$copy = clone $this;
|
||||
$copy->fields = null;
|
||||
|
||||
foreach ($keys as $key) {
|
||||
unset($copy->data[$key]);
|
||||
}
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent
|
||||
* Site, Page, File or User object
|
||||
*
|
||||
* @return \Kirby\Cms\Model
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parent model
|
||||
*
|
||||
* @param \Kirby\Cms\Model $parent
|
||||
* @return self
|
||||
*/
|
||||
public function setParent(Model $parent)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data array
|
||||
*
|
||||
* @see self::data()
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->data();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the content and returns
|
||||
* a cloned object
|
||||
*
|
||||
* @param array $content
|
||||
* @param bool $overwrite
|
||||
* @return self
|
||||
*/
|
||||
public function update(array $content = null, bool $overwrite = false)
|
||||
{
|
||||
$this->data = $overwrite === true ? (array)$content : array_merge($this->data, (array)$content);
|
||||
|
||||
// clear cache of Field objects
|
||||
$this->fields = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
229
kirby/src/Cms/ContentLock.php
Normal file
229
kirby/src/Cms/ContentLock.php
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
|
||||
/**
|
||||
* Takes care of content lock and unlock information
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class ContentLock
|
||||
{
|
||||
/**
|
||||
* Lock data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* The model to manage locking/unlocking for
|
||||
*
|
||||
* @var ModelWithContent
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\ModelWithContent $model
|
||||
*/
|
||||
public function __construct(ModelWithContent $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->data = $this->kirby()->locks()->get($model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the lock unconditionally
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function clearLock(): bool
|
||||
{
|
||||
// if no lock exists, skip
|
||||
if (isset($this->data['lock']) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// remove lock
|
||||
unset($this->data['lock']);
|
||||
|
||||
return $this->kirby()->locks()->set($this->model, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets lock with the current user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function create(): bool
|
||||
{
|
||||
// check if model is already locked by another user
|
||||
if (
|
||||
isset($this->data['lock']) === true &&
|
||||
$this->data['lock']['user'] !== $this->user()->id()
|
||||
) {
|
||||
$id = ContentLocks::id($this->model);
|
||||
throw new DuplicateException($id . ' is already locked');
|
||||
}
|
||||
|
||||
$this->data['lock'] = [
|
||||
'user' => $this->user()->id(),
|
||||
'time' => time()
|
||||
];
|
||||
|
||||
return $this->kirby()->locks()->set($this->model, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either `false` or array with `user`, `email`,
|
||||
* `time` and `unlockable` keys
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function get()
|
||||
{
|
||||
$data = $this->data['lock'] ?? [];
|
||||
|
||||
if (empty($data) === false && $data['user'] !== $this->user()->id()) {
|
||||
if ($user = $this->kirby()->user($data['user'])) {
|
||||
$time = (int)($data['time']);
|
||||
|
||||
return [
|
||||
'user' => $user->id(),
|
||||
'email' => $user->email(),
|
||||
'time' => $time,
|
||||
'unlockable' => ($time + 60) <= time()
|
||||
];
|
||||
}
|
||||
|
||||
// clear lock if user not found
|
||||
$this->clearLock();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the model is locked by another user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocked(): bool
|
||||
{
|
||||
$lock = $this->get();
|
||||
|
||||
if ($lock !== false && $lock['user'] !== $this->user()->id()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the current user's lock has been removed by another user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isUnlocked(): bool
|
||||
{
|
||||
$data = $this->data['unlock'] ?? [];
|
||||
|
||||
return in_array($this->user()->id(), $data) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app instance
|
||||
*
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
protected function kirby(): App
|
||||
{
|
||||
return $this->model->kirby();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes lock of current user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function remove(): bool
|
||||
{
|
||||
// if no lock exists, skip
|
||||
if (isset($this->data['lock']) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if lock was set by another user
|
||||
if ($this->data['lock']['user'] !== $this->user()->id()) {
|
||||
throw new LogicException([
|
||||
'fallback' => 'The content lock can only be removed by the user who created it. Use unlock instead.',
|
||||
'httpCode' => 409
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->clearLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes unlock information for current user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function resolve(): bool
|
||||
{
|
||||
// if no unlocks exist, skip
|
||||
if (isset($this->data['unlock']) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// remove user from unlock array
|
||||
$this->data['unlock'] = array_diff(
|
||||
$this->data['unlock'],
|
||||
[$this->user()->id()]
|
||||
);
|
||||
|
||||
return $this->kirby()->locks()->set($this->model, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes current lock and adds lock user to unlock data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function unlock(): bool
|
||||
{
|
||||
// if no lock exists, skip
|
||||
if (isset($this->data['lock']) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// add lock user to unlocked data
|
||||
$this->data['unlock'] = $this->data['unlock'] ?? [];
|
||||
$this->data['unlock'][] = $this->data['lock']['user'];
|
||||
|
||||
return $this->clearLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns currently authenticated user;
|
||||
* throws exception if none is authenticated
|
||||
*
|
||||
* @return \Kirby\Cms\User
|
||||
*/
|
||||
protected function user(): User
|
||||
{
|
||||
if ($user = $this->kirby()->user()) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
throw new PermissionException('No user authenticated.');
|
||||
}
|
||||
}
|
||||
225
kirby/src/Cms/ContentLocks.php
Normal file
225
kirby/src/Cms/ContentLocks.php
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* Manages all content lock files
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Nico Hoffmann <nico@getkirby.com>,
|
||||
* Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class ContentLocks
|
||||
{
|
||||
/**
|
||||
* Data from the `.lock` files
|
||||
* that have been read so far
|
||||
* cached by `.lock` file path
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* PHP file handles for all currently
|
||||
* open `.lock` files
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $handles = [];
|
||||
|
||||
/**
|
||||
* Closes the open file handles
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
foreach ($this->handles as $file => $handle) {
|
||||
$this->closeHandle($file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the file lock and closes the file handle
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
protected function closeHandle(string $file)
|
||||
{
|
||||
if (isset($this->handles[$file]) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = $this->handles[$file];
|
||||
$result = flock($handle, LOCK_UN) && fclose($handle);
|
||||
|
||||
if ($result !== true) {
|
||||
throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
unset($this->handles[$file]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to a model's lock file
|
||||
*
|
||||
* @param \Kirby\Cms\ModelWithContent $model
|
||||
* @return string
|
||||
*/
|
||||
public static function file(ModelWithContent $model): string
|
||||
{
|
||||
return $model->contentFileDirectory() . '/.lock';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lock/unlock data for the specified model
|
||||
*
|
||||
* @param \Kirby\Cms\ModelWithContent $model
|
||||
* @return array
|
||||
*/
|
||||
public function get(ModelWithContent $model): array
|
||||
{
|
||||
$file = static::file($model);
|
||||
$id = static::id($model);
|
||||
|
||||
// return from cache if file was already loaded
|
||||
if (isset($this->data[$file]) === true) {
|
||||
return $this->data[$file][$id] ?? [];
|
||||
}
|
||||
|
||||
// first get a handle to ensure a file system lock
|
||||
$handle = $this->handle($file);
|
||||
|
||||
if (is_resource($handle) === true) {
|
||||
// read data from file
|
||||
clearstatcache();
|
||||
$filesize = filesize($file);
|
||||
|
||||
if ($filesize > 0) {
|
||||
// always read the whole file
|
||||
rewind($handle);
|
||||
$string = fread($handle, $filesize);
|
||||
$data = Data::decode($string, 'yaml');
|
||||
}
|
||||
}
|
||||
|
||||
$this->data[$file] = $data ?? [];
|
||||
|
||||
return $this->data[$file][$id] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file handle to a `.lock` file
|
||||
*
|
||||
* @param string $file
|
||||
* @param bool $create Whether to create the file if it does not exist
|
||||
* @return resource|null File handle
|
||||
*/
|
||||
protected function handle(string $file, bool $create = false)
|
||||
{
|
||||
// check for an already open handle
|
||||
if (isset($this->handles[$file]) === true) {
|
||||
return $this->handles[$file];
|
||||
}
|
||||
|
||||
// don't create a file if not requested
|
||||
if (is_file($file) !== true && $create !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$handle = @fopen($file, 'c+b');
|
||||
if (is_resource($handle) === false) {
|
||||
throw new Exception('Lock file ' . $file . ' could not be opened.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// lock the lock file exclusively to prevent changes by other threads
|
||||
$result = flock($handle, LOCK_EX);
|
||||
if ($result !== true) {
|
||||
throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return $this->handles[$file] = $handle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns model ID used as the key for the data array;
|
||||
* prepended with a slash because the $site otherwise won't have an ID
|
||||
*
|
||||
* @param \Kirby\Cms\ModelWithContent $model
|
||||
* @return string
|
||||
*/
|
||||
public static function id(ModelWithContent $model): string
|
||||
{
|
||||
return '/' . $model->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets and writes the lock/unlock data for the specified model
|
||||
*
|
||||
* @param \Kirby\Cms\ModelWithContent $model
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function set(ModelWithContent $model, array $data): bool
|
||||
{
|
||||
$file = static::file($model);
|
||||
$id = static::id($model);
|
||||
$handle = $this->handle($file, true);
|
||||
|
||||
$this->data[$file][$id] = $data;
|
||||
|
||||
// make sure to unset model id entries,
|
||||
// if no lock data for the model exists
|
||||
foreach ($this->data[$file] as $id => $data) {
|
||||
// there is no data for that model whatsoever
|
||||
if (
|
||||
isset($data['lock']) === false &&
|
||||
(isset($data['unlock']) === false ||
|
||||
count($data['unlock']) === 0)
|
||||
) {
|
||||
unset($this->data[$file][$id]);
|
||||
|
||||
// there is empty unlock data, but still lock data
|
||||
} elseif (
|
||||
isset($data['unlock']) === true &&
|
||||
count($data['unlock']) === 0
|
||||
) {
|
||||
unset($this->data[$file][$id]['unlock']);
|
||||
}
|
||||
}
|
||||
|
||||
// there is no data left in the file whatsoever, delete the file
|
||||
if (count($this->data[$file]) === 0) {
|
||||
unset($this->data[$file]);
|
||||
|
||||
// close the file handle, otherwise we can't delete it on Windows
|
||||
$this->closeHandle($file);
|
||||
|
||||
return F::remove($file);
|
||||
}
|
||||
|
||||
$yaml = Data::encode($this->data[$file], 'yaml');
|
||||
|
||||
// delete all file contents first
|
||||
if (rewind($handle) !== true || ftruncate($handle, 0) !== true) {
|
||||
throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
// write the new contents
|
||||
$result = fwrite($handle, $yaml);
|
||||
if (is_int($result) === false || $result === 0) {
|
||||
throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
242
kirby/src/Cms/ContentTranslation.php
Normal file
242
kirby/src/Cms/ContentTranslation.php
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
||||
/**
|
||||
* Each page, file or site can have multiple
|
||||
* translated versions of their content,
|
||||
* represented by this class
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class ContentTranslation
|
||||
{
|
||||
use Properties;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $contentFile;
|
||||
|
||||
/**
|
||||
* @var Model
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $slug;
|
||||
|
||||
/**
|
||||
* Creates a new translation object
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->setRequiredProperties($props, ['parent', 'code']);
|
||||
$this->setOptionalProperties($props, ['slug', 'content']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language code of the
|
||||
* translation
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function code(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation content
|
||||
* as plain array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function content(): array
|
||||
{
|
||||
$parent = $this->parent();
|
||||
|
||||
if ($this->content === null) {
|
||||
$this->content = $parent->readContent($this->code());
|
||||
}
|
||||
|
||||
$content = $this->content;
|
||||
|
||||
// merge with the default content
|
||||
if ($this->isDefault() === false && $defaultLanguage = $parent->kirby()->defaultLanguage()) {
|
||||
$default = [];
|
||||
|
||||
if ($defaultTranslation = $parent->translation($defaultLanguage->code())) {
|
||||
$default = $defaultTranslation->content();
|
||||
}
|
||||
|
||||
$content = array_merge($default, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to the translation content file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function contentFile(): string
|
||||
{
|
||||
return $this->contentFile = $this->parent->contentFile($this->code, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the translation file exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->contentFile()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation code as id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the this is the default translation
|
||||
* of the model
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefault(): bool
|
||||
{
|
||||
if ($defaultLanguage = $this->parent->kirby()->defaultLanguage()) {
|
||||
return $this->code() === $defaultLanguage->code();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent page, file or site object
|
||||
*
|
||||
* @return \Kirby\Cms\Model
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @return self
|
||||
*/
|
||||
protected function setCode(string $code)
|
||||
{
|
||||
$this->code = $code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $content
|
||||
* @return self
|
||||
*/
|
||||
protected function setContent(array $content = null)
|
||||
{
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\Model $parent
|
||||
* @return self
|
||||
*/
|
||||
protected function setParent(Model $parent)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slug
|
||||
* @return self
|
||||
*/
|
||||
protected function setSlug(string $slug = null)
|
||||
{
|
||||
$this->slug = $slug;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom translation slug
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function slug(): ?string
|
||||
{
|
||||
return $this->slug = $this->slug ?? ($this->content()['slug'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the old and new data
|
||||
*
|
||||
* @param array|null $data
|
||||
* @param bool $overwrite
|
||||
* @return self
|
||||
*/
|
||||
public function update(array $data = null, bool $overwrite = false)
|
||||
{
|
||||
$this->content = $overwrite === true ? (array)$data : array_merge($this->content(), (array)$data);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the most imporant translation
|
||||
* props to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->code(),
|
||||
'content' => $this->content(),
|
||||
'exists' => $this->exists(),
|
||||
'slug' => $this->slug(),
|
||||
];
|
||||
}
|
||||
}
|
||||
180
kirby/src/Cms/Dir.php
Normal file
180
kirby/src/Cms/Dir.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of the Toolkit `Dir` class with a new
|
||||
* `Dir::inventory` method, that handles scanning directories
|
||||
* and converts the results into our children, files and
|
||||
* other page stuff.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Dir extends \Kirby\Toolkit\Dir
|
||||
{
|
||||
public static $numSeparator = '_';
|
||||
|
||||
/**
|
||||
* Scans the directory and analyzes files,
|
||||
* content, meta info and children. This is used
|
||||
* in Page, Site and User objects to fetch all
|
||||
* relevant information.
|
||||
*
|
||||
* @param string $dir
|
||||
* @param string $contentExtension
|
||||
* @param array $contentIgnore
|
||||
* @param bool $multilang
|
||||
* @return array
|
||||
*/
|
||||
public static function inventory(string $dir, string $contentExtension = 'txt', array $contentIgnore = null, bool $multilang = false): array
|
||||
{
|
||||
$dir = realpath($dir);
|
||||
|
||||
$inventory = [
|
||||
'children' => [],
|
||||
'files' => [],
|
||||
'template' => 'default',
|
||||
];
|
||||
|
||||
if ($dir === false) {
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
$items = Dir::read($dir, $contentIgnore);
|
||||
|
||||
// a temporary store for all content files
|
||||
$content = [];
|
||||
|
||||
// sort all items naturally to avoid sorting issues later
|
||||
natsort($items);
|
||||
|
||||
foreach ($items as $item) {
|
||||
|
||||
// ignore all items with a leading dot
|
||||
if (in_array(substr($item, 0, 1), ['.', '_']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$root = $dir . '/' . $item;
|
||||
|
||||
if (is_dir($root) === true) {
|
||||
|
||||
// extract the slug and num of the directory
|
||||
if (preg_match('/^([0-9]+)' . static::$numSeparator . '(.*)$/', $item, $match)) {
|
||||
$num = $match[1];
|
||||
$slug = $match[2];
|
||||
} else {
|
||||
$num = null;
|
||||
$slug = $item;
|
||||
}
|
||||
|
||||
$inventory['children'][] = [
|
||||
'dirname' => $item,
|
||||
'model' => null,
|
||||
'num' => $num,
|
||||
'root' => $root,
|
||||
'slug' => $slug,
|
||||
];
|
||||
} else {
|
||||
$extension = pathinfo($item, PATHINFO_EXTENSION);
|
||||
|
||||
switch ($extension) {
|
||||
case 'htm':
|
||||
case 'html':
|
||||
case 'php':
|
||||
// don't track those files
|
||||
break;
|
||||
case $contentExtension:
|
||||
$content[] = pathinfo($item, PATHINFO_FILENAME);
|
||||
break;
|
||||
default:
|
||||
$inventory['files'][$item] = [
|
||||
'filename' => $item,
|
||||
'extension' => $extension,
|
||||
'root' => $root,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove the language codes from all content filenames
|
||||
if ($multilang === true) {
|
||||
foreach ($content as $key => $filename) {
|
||||
$content[$key] = pathinfo($filename, PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
$content = array_unique($content);
|
||||
}
|
||||
|
||||
$inventory = static::inventoryContent($inventory, $content);
|
||||
$inventory = static::inventoryModels($inventory, $contentExtension, $multilang);
|
||||
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take all content files,
|
||||
* remove those who are meta files and
|
||||
* detect the main content file
|
||||
*
|
||||
* @param array $inventory
|
||||
* @param array $content
|
||||
* @return array
|
||||
*/
|
||||
protected static function inventoryContent(array $inventory, array $content): array
|
||||
{
|
||||
|
||||
// filter meta files from the content file
|
||||
if (empty($content) === true) {
|
||||
$inventory['template'] = 'default';
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
foreach ($content as $contentName) {
|
||||
|
||||
// could be a meta file. i.e. cover.jpg
|
||||
if (isset($inventory['files'][$contentName]) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// it's most likely the template
|
||||
$inventory['template'] = $contentName;
|
||||
}
|
||||
|
||||
return $inventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Go through all inventory children
|
||||
* and inject a model for each
|
||||
*
|
||||
* @param array $inventory
|
||||
* @param string $contentExtension
|
||||
* @param bool $multilang
|
||||
* @return array
|
||||
*/
|
||||
protected static function inventoryModels(array $inventory, string $contentExtension, bool $multilang = false): array
|
||||
{
|
||||
// inject models
|
||||
if (empty($inventory['children']) === false && empty(Page::$models) === false) {
|
||||
if ($multilang === true) {
|
||||
$contentExtension = App::instance()->defaultLanguage()->code() . '.' . $contentExtension;
|
||||
}
|
||||
|
||||
foreach ($inventory['children'] as $key => $child) {
|
||||
foreach (Page::$models as $modelName => $modelClass) {
|
||||
if (file_exists($child['root'] . '/' . $modelName . '.' . $contentExtension) === true) {
|
||||
$inventory['children'][$key]['model'] = $modelName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $inventory;
|
||||
}
|
||||
}
|
||||
248
kirby/src/Cms/Email.php
Normal file
248
kirby/src/Cms/Email.php
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
|
||||
/**
|
||||
* Wrapper around our Email package, which
|
||||
* handles all the magic connections between Kirby
|
||||
* and sending emails, like email templates, file
|
||||
* attachments, etc.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Email
|
||||
{
|
||||
/**
|
||||
* Options configured through the `email` CMS option
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* Props for the email object; will be passed to the
|
||||
* Kirby\Email\Email class
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $props;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string|array $preset Preset name from the config or a simple props array
|
||||
* @param array $props Props array to override the $preset
|
||||
*/
|
||||
public function __construct($preset = [], array $props = [])
|
||||
{
|
||||
$this->options = App::instance()->option('email');
|
||||
|
||||
// build a prop array based on preset and props
|
||||
$preset = $this->preset($preset);
|
||||
$this->props = array_merge($preset, $props);
|
||||
|
||||
// add transport settings
|
||||
if (isset($this->props['transport']) === false) {
|
||||
$this->props['transport'] = $this->options['transport'] ?? [];
|
||||
}
|
||||
|
||||
// transform model objects to values
|
||||
$this->transformUserSingle('from', 'fromName');
|
||||
$this->transformUserSingle('replyTo', 'replyToName');
|
||||
$this->transformUserMultiple('to');
|
||||
$this->transformUserMultiple('cc');
|
||||
$this->transformUserMultiple('bcc');
|
||||
$this->transformFile('attachments');
|
||||
|
||||
// load template for body text
|
||||
$this->template();
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs a preset from the options; supports fixed
|
||||
* prop arrays in case a preset is not needed
|
||||
*
|
||||
* @param string|array $preset Preset name or simple prop array
|
||||
* @return array
|
||||
*/
|
||||
protected function preset($preset): array
|
||||
{
|
||||
// only passed props, not preset name
|
||||
if (is_array($preset) === true) {
|
||||
return $preset;
|
||||
}
|
||||
|
||||
// preset does not exist
|
||||
if (isset($this->options['presets'][$preset]) !== true) {
|
||||
throw new NotFoundException([
|
||||
'key' => 'email.preset.notFound',
|
||||
'data' => ['name' => $preset]
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->options['presets'][$preset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the email template(s) and sets the body props
|
||||
* to the result
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function template(): void
|
||||
{
|
||||
if (isset($this->props['template']) === true) {
|
||||
|
||||
// prepare data to be passed to template
|
||||
$data = $this->props['data'] ?? [];
|
||||
|
||||
// check if html/text templates exist
|
||||
$html = $this->getTemplate($this->props['template'], 'html');
|
||||
$text = $this->getTemplate($this->props['template'], 'text');
|
||||
|
||||
if ($html->exists()) {
|
||||
$this->props['body'] = [
|
||||
'html' => $html->render($data)
|
||||
];
|
||||
|
||||
if ($text->exists()) {
|
||||
$this->props['body']['text'] = $text->render($data);
|
||||
}
|
||||
|
||||
// fallback to single email text template
|
||||
} elseif ($text->exists()) {
|
||||
$this->props['body'] = $text->render($data);
|
||||
} else {
|
||||
throw new NotFoundException('The email template "' . $this->props['template'] . '" cannot be found');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an email template by name and type
|
||||
*
|
||||
* @param string $name Template name
|
||||
* @param string|null $type `html` or `text`
|
||||
* @return \Kirby\Cms\Template
|
||||
*/
|
||||
protected function getTemplate(string $name, string $type = null)
|
||||
{
|
||||
return App::instance()->template('emails/' . $name, $type, 'text');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the prop array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms file object(s) to an array of file roots;
|
||||
* supports simple strings, file objects or collections/arrays of either
|
||||
*
|
||||
* @param string $prop Prop to transform
|
||||
* @return void
|
||||
*/
|
||||
protected function transformFile(string $prop): void
|
||||
{
|
||||
$this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\File', 'root');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Kirby models to a simplified collection
|
||||
*
|
||||
* @param string $prop Prop to transform
|
||||
* @param string $class Fully qualified class name of the supported model
|
||||
* @param string $contentValue Model method that returns the array value
|
||||
* @param string|null $contentKey Optional model method that returns the array key;
|
||||
* returns a simple value-only array if not given
|
||||
* @return array Simple key-value or just value array with the transformed prop data
|
||||
*/
|
||||
protected function transformModel(string $prop, string $class, string $contentValue, string $contentKey = null): array
|
||||
{
|
||||
$value = $this->props[$prop] ?? [];
|
||||
|
||||
// ensure consistent input by making everything an iterable value
|
||||
if (is_iterable($value) !== true) {
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($value as $key => $item) {
|
||||
if (is_string($item) === true) {
|
||||
// value is already a string
|
||||
if ($contentKey !== null && is_string($key) === true) {
|
||||
$result[$key] = $item;
|
||||
} else {
|
||||
$result[] = $item;
|
||||
}
|
||||
} elseif (is_a($item, $class) === true) {
|
||||
// value is a model object, get value through content method(s)
|
||||
if ($contentKey !== null) {
|
||||
$result[(string)$item->$contentKey()] = (string)$item->$contentValue();
|
||||
} else {
|
||||
$result[] = (string)$item->$contentValue();
|
||||
}
|
||||
} else {
|
||||
// invalid input
|
||||
throw new InvalidArgumentException('Invalid input for prop "' . $prop . '", expected string or "' . $class . '" object or collection');
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an user object to the email address and name;
|
||||
* supports simple strings, user objects or collections/arrays of either
|
||||
* (note: only the first item in a collection/array will be used)
|
||||
*
|
||||
* @param string $addressProp Prop with the email address
|
||||
* @param string $nameProp Prop with the name corresponding to the $addressProp
|
||||
* @return void
|
||||
*/
|
||||
protected function transformUserSingle(string $addressProp, string $nameProp): void
|
||||
{
|
||||
$result = $this->transformModel($addressProp, 'Kirby\Cms\User', 'name', 'email');
|
||||
|
||||
$address = array_keys($result)[0] ?? null;
|
||||
$name = $result[$address] ?? null;
|
||||
|
||||
// if the array is non-associative, the value is the address
|
||||
if (is_int($address) === true) {
|
||||
$address = $name;
|
||||
$name = null;
|
||||
}
|
||||
|
||||
// always use the address as we have transformed that prop above
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms user object(s) to the email address(es) and name(s);
|
||||
* supports simple strings, user objects or collections/arrays of either
|
||||
*
|
||||
* @param string $prop Prop to transform
|
||||
* @return void
|
||||
*/
|
||||
protected function transformUserMultiple(string $prop): void
|
||||
{
|
||||
$this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\User', 'name', 'email');
|
||||
}
|
||||
}
|
||||
288
kirby/src/Cms/Event.php
Normal file
288
kirby/src/Cms/Event.php
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Controller;
|
||||
|
||||
/**
|
||||
* The Event object is created whenever the `$kirby->trigger()`
|
||||
* or `$kirby->apply()` methods are called. It collects all
|
||||
* event information and handles calling the individual hooks.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Lukas Bestle <lukas@getkirby.com>,
|
||||
* Ahmet Bora
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Event
|
||||
{
|
||||
/**
|
||||
* The full event name
|
||||
* (e.g. `page.create:after`)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The event type
|
||||
* (e.g. `page` in `page.create:after`)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* The event action
|
||||
* (e.g. `create` in `page.create:after`)
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $action;
|
||||
|
||||
/**
|
||||
* The event state
|
||||
* (e.g. `after` in `page.create:after`)
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The event arguments
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $arguments = [];
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string $name Full event name
|
||||
* @param array $arguments Associative array of named event arguments
|
||||
*/
|
||||
public function __construct(string $name, array $arguments = [])
|
||||
{
|
||||
// split the event name into `$type.$action:$state`
|
||||
// $action and $state are optional;
|
||||
// if there is more than one dot, $type will be greedy
|
||||
$regex = '/^(?<type>.+?)(?:\.(?<action>[^.]*?))?(?:\:(?<state>.*))?$/';
|
||||
preg_match($regex, $name, $matches, PREG_UNMATCHED_AS_NULL);
|
||||
|
||||
$this->name = $name;
|
||||
$this->type = $matches['type'];
|
||||
$this->action = $matches['action'] ?? null;
|
||||
$this->state = $matches['state'] ?? null;
|
||||
$this->arguments = $arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic caller for event arguments
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
return $this->argument($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes it possible to simply echo
|
||||
* or stringify the entire object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the action of the event (e.g. `create`)
|
||||
* or `null` if the event name does not include an action
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function action(): ?string
|
||||
{
|
||||
return $this->action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific event argument
|
||||
*
|
||||
* @param string $name
|
||||
* @return mixed
|
||||
*/
|
||||
public function argument(string $name)
|
||||
{
|
||||
if (isset($this->arguments[$name]) === true) {
|
||||
return $this->arguments[$name];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the arguments of the event
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function arguments(): array
|
||||
{
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a hook with the event data and returns
|
||||
* the hook's return value
|
||||
*
|
||||
* @param object|null $bind Optional object to bind to the hook function
|
||||
* @param \Closure $hook
|
||||
* @return mixed
|
||||
*/
|
||||
public function call($bind = null, Closure $hook)
|
||||
{
|
||||
// collect the list of possible hook arguments
|
||||
$data = $this->arguments();
|
||||
$data['event'] = $this;
|
||||
|
||||
// magically call the hook with the arguments it requested
|
||||
$hook = new Controller($hook);
|
||||
return $hook->call($bind, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full name of the event
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full list of possible wildcard
|
||||
* event names based on the current event name
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function nameWildcards(): array
|
||||
{
|
||||
// if the event is already a wildcard event, no further variation is possible
|
||||
if ($this->type === '*' || $this->action === '*' || $this->state === '*') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->action !== null && $this->state !== null) {
|
||||
// full $type.$action:$state event
|
||||
|
||||
return [
|
||||
$this->type . '.*:' . $this->state,
|
||||
$this->type . '.' . $this->action . ':*',
|
||||
$this->type . '.*:*',
|
||||
'*.' . $this->action . ':' . $this->state,
|
||||
'*.' . $this->action . ':*',
|
||||
'*:' . $this->state,
|
||||
'*'
|
||||
];
|
||||
} elseif ($this->state !== null) {
|
||||
// event without action: $type:$state
|
||||
|
||||
return [
|
||||
$this->type . ':*',
|
||||
'*:' . $this->state,
|
||||
'*'
|
||||
];
|
||||
} elseif ($this->action !== null) {
|
||||
// event without state: $type.$action
|
||||
|
||||
return [
|
||||
$this->type . '.*',
|
||||
'*.' . $this->action,
|
||||
'*'
|
||||
];
|
||||
} else {
|
||||
// event with a simple name
|
||||
|
||||
return ['*'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the state of the event (e.g. `after`)
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function state(): ?string
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the event data as array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'arguments' => $this->arguments
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the event name as string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the event (e.g. `page`)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a given argument with a new value
|
||||
*
|
||||
* @internal
|
||||
* @param string $name
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function updateArgument(string $name, $value): void
|
||||
{
|
||||
if (array_key_exists($name, $this->arguments) !== true) {
|
||||
throw new InvalidArgumentException('The argument ' . $name . ' does not exist');
|
||||
}
|
||||
|
||||
$this->arguments[$name] = $value;
|
||||
}
|
||||
}
|
||||
257
kirby/src/Cms/Field.php
Normal file
257
kirby/src/Cms/Field.php
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Every field in a Kirby content text file
|
||||
* is being converted into such a Field object.
|
||||
*
|
||||
* Field methods can be registered for those Field
|
||||
* objects, which can then be used to transform or
|
||||
* convert the field value. This enables our
|
||||
* daisy-chaining API for templates and other components
|
||||
*
|
||||
* ```php
|
||||
* // Page field example with lowercase conversion
|
||||
* $page->myField()->lower();
|
||||
* ```
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Field
|
||||
{
|
||||
/**
|
||||
* Field method aliases
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $aliases = [];
|
||||
|
||||
/**
|
||||
* The field name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* Registered field methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $methods = [];
|
||||
|
||||
/**
|
||||
* The parent object if available.
|
||||
* This will be the page, site, user or file
|
||||
* to which the content belongs
|
||||
*
|
||||
* @var Model
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* The value of the field
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* Magic caller for field methods
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
$method = strtolower($method);
|
||||
|
||||
if (isset(static::$methods[$method]) === true) {
|
||||
return static::$methods[$method](clone $this, ...$arguments);
|
||||
}
|
||||
|
||||
if (isset(static::$aliases[$method]) === true) {
|
||||
$method = strtolower(static::$aliases[$method]);
|
||||
|
||||
if (isset(static::$methods[$method]) === true) {
|
||||
return static::$methods[$method](clone $this, ...$arguments);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new field object
|
||||
*
|
||||
* @param object $parent
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct($parent = null, string $key, $value)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->value = $value;
|
||||
$this->parent = $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies the var_dump result
|
||||
*
|
||||
* @see Field::toArray
|
||||
* @return void
|
||||
*/
|
||||
public function __debugInfo()
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes it possible to simply echo
|
||||
* or stringify the entire object
|
||||
*
|
||||
* @see Field::toString
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field exists in the content data array
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return $this->parent->content()->has($this->key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field content is empty
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->value) === true && in_array($this->value, [0, '0', false], true) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field content is not empty
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNotEmpty(): bool
|
||||
{
|
||||
return $this->isEmpty() === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Field::parent()
|
||||
* @return \Kirby\Cms\Model|null
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a fallback if the field value is empty
|
||||
*
|
||||
* @param mixed $fallback
|
||||
* @return self
|
||||
*/
|
||||
public function or($fallback = null)
|
||||
{
|
||||
if ($this->isNotEmpty()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (is_a($fallback, 'Kirby\Cms\Field') === true) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$field = clone $this;
|
||||
$field->value = $fallback;
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent object of the field
|
||||
*
|
||||
* @return \Kirby\Cms\Model|null
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the Field object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [$this->key => $this->value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field value as string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return (string)$this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field content. If a new value is passed,
|
||||
* the modified field will be returned. Otherwise it
|
||||
* will return the field value.
|
||||
*
|
||||
* @param string|Closure $value
|
||||
* @return mixed
|
||||
*/
|
||||
public function value($value = null)
|
||||
{
|
||||
if ($value === null) {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
if (is_scalar($value)) {
|
||||
$value = (string)$value;
|
||||
} elseif (is_callable($value)) {
|
||||
$value = (string)$value->call($this, $this->value);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid field value type: ' . gettype($value));
|
||||
}
|
||||
|
||||
$clone = clone $this;
|
||||
$clone->value = $value;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
}
|
||||
787
kirby/src/Cms/File.php
Normal file
787
kirby/src/Cms/File.php
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* The `$file` object provides a set
|
||||
* of methods that can be used when
|
||||
* dealing with a single image or
|
||||
* other media file, like getting the
|
||||
* URL or resizing an image. It also
|
||||
* handles file meta data.
|
||||
*
|
||||
* The File class is a wrapper around
|
||||
* the Kirby\Image\Image class, which
|
||||
* is used to handle all file methods.
|
||||
* In addition the File class handles
|
||||
* File meta data via Kirby\Cms\Content.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class File extends ModelWithContent
|
||||
{
|
||||
const CLASS_ALIAS = 'file';
|
||||
|
||||
use FileActions;
|
||||
use FileFoundation;
|
||||
use FileModifications;
|
||||
use HasMethods;
|
||||
use HasSiblings;
|
||||
|
||||
/**
|
||||
* The parent asset object
|
||||
* This is used to do actual file
|
||||
* method calls, like size, mime, etc.
|
||||
*
|
||||
* @var \Kirby\Image\Image
|
||||
*/
|
||||
protected $asset;
|
||||
|
||||
/**
|
||||
* Cache for the initialized blueprint object
|
||||
*
|
||||
* @var \Kirby\Cms\FileBlueprint
|
||||
*/
|
||||
protected $blueprint;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filename;
|
||||
|
||||
/**
|
||||
* All registered file methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $methods = [];
|
||||
|
||||
/**
|
||||
* The parent object
|
||||
*
|
||||
* @var \Kirby\Cms\Model
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* The absolute path to the file
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $template;
|
||||
|
||||
/**
|
||||
* The public file Url
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Magic caller for file methods
|
||||
* and content fields. (in this order)
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
// public property access
|
||||
if (isset($this->$method) === true) {
|
||||
return $this->$method;
|
||||
}
|
||||
|
||||
// asset method proxy
|
||||
if (method_exists($this->asset(), $method)) {
|
||||
return $this->asset()->$method(...$arguments);
|
||||
}
|
||||
|
||||
// file methods
|
||||
if ($this->hasMethod($method)) {
|
||||
return $this->callMethod($method, $arguments);
|
||||
}
|
||||
|
||||
// content fields
|
||||
return $this->content()->get($method, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new File object
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
// properties
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return array_merge($this->toArray(), [
|
||||
'content' => $this->content(),
|
||||
'siblings' => $this->siblings(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to api endpoint
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
*/
|
||||
public function apiUrl(bool $relative = false): string
|
||||
{
|
||||
return $this->parent()->apiUrl($relative) . '/files/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Image object
|
||||
*
|
||||
* @internal
|
||||
* @return \Kirby\Image\Image
|
||||
*/
|
||||
public function asset()
|
||||
{
|
||||
return $this->asset = $this->asset ?? new Image($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FileBlueprint object for the file
|
||||
*
|
||||
* @return \Kirby\Cms\FileBlueprint
|
||||
*/
|
||||
public function blueprint()
|
||||
{
|
||||
if (is_a($this->blueprint, 'Kirby\Cms\FileBlueprint') === true) {
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
return $this->blueprint = FileBlueprint::factory('files/' . $this->template(), 'files/default', $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the template in addition to the
|
||||
* other content.
|
||||
*
|
||||
* @internal
|
||||
* @param array $data
|
||||
* @param string|null $languageCode
|
||||
* @return array
|
||||
*/
|
||||
public function contentFileData(array $data, string $languageCode = null): array
|
||||
{
|
||||
return A::append($data, [
|
||||
'template' => $this->template(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory in which
|
||||
* the content file is located
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileDirectory(): string
|
||||
{
|
||||
return dirname($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename for the content file
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileName(): string
|
||||
{
|
||||
return $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a kirbytag or markdown
|
||||
* tag for the file, which will be
|
||||
* used in the panel, when the file
|
||||
* gets dragged onto a textarea
|
||||
*
|
||||
* @internal
|
||||
* @param string $type (null|auto|kirbytext|markdown)
|
||||
* @param bool $absolute
|
||||
* @return string
|
||||
*/
|
||||
public function dragText(string $type = null, bool $absolute = false): string
|
||||
{
|
||||
$type = $type ?? 'auto';
|
||||
|
||||
if ($type === 'auto') {
|
||||
$type = option('panel.kirbytext', true) ? 'kirbytext' : 'markdown';
|
||||
}
|
||||
|
||||
$url = $absolute ? $this->id() : $this->filename();
|
||||
|
||||
switch ($type) {
|
||||
case 'markdown':
|
||||
if ($this->type() === 'image') {
|
||||
return '';
|
||||
} else {
|
||||
return '[' . $this->filename() . '](' . $url . ')';
|
||||
}
|
||||
// no break
|
||||
default:
|
||||
if ($this->type() === 'image') {
|
||||
return '(image: ' . $url . ')';
|
||||
} else {
|
||||
return '(file: ' . $url . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a File object
|
||||
*
|
||||
* @internal
|
||||
* @param mixed $props
|
||||
* @return self
|
||||
*/
|
||||
public static function factory($props)
|
||||
{
|
||||
return new static($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename with extension
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function filename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Files collection
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function files()
|
||||
{
|
||||
return $this->siblingsCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
if ($this->id !== null) {
|
||||
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) {
|
||||
return $this->id = $this->parent()->id() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
return $this->id = $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the current object with the given file object
|
||||
*
|
||||
* @param \Kirby\Cms\File $file
|
||||
* @return bool
|
||||
*/
|
||||
public function is(File $file): bool
|
||||
{
|
||||
return $this->id() === $file->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the file can be read by the current user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReadable(): bool
|
||||
{
|
||||
static $readable = [];
|
||||
|
||||
$template = $this->template();
|
||||
|
||||
if (isset($readable[$template]) === true) {
|
||||
return $readable[$template];
|
||||
}
|
||||
|
||||
return $readable[$template] = $this->permissions()->can('read');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique media hash
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function mediaHash(): string
|
||||
{
|
||||
return $this->mediaToken() . '-' . $this->modifiedFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file in the public media folder
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return $this->parent()->mediaRoot() . '/' . $this->mediaHash() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a non-guessable token string for this file
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function mediaToken(): string
|
||||
{
|
||||
$token = $this->kirby()->contentToken($this, $this->id());
|
||||
return substr($token, 0, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute Url to the file in the public media folder
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
return $this->parent()->mediaUrl() . '/' . $this->mediaHash() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `File::content()` instead
|
||||
*
|
||||
* @return \Kirby\Cms\Content
|
||||
*/
|
||||
public function meta()
|
||||
{
|
||||
deprecated('$file->meta() is deprecated, use $file->content() instead. $file->meta() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->content();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's last modification time.
|
||||
*
|
||||
* @param string $format
|
||||
* @param string|null $handler date or strftime
|
||||
* @param string|null $languageCode
|
||||
* @return mixed
|
||||
*/
|
||||
public function modified(string $format = null, string $handler = null, string $languageCode = null)
|
||||
{
|
||||
$file = $this->modifiedFile();
|
||||
$content = $this->modifiedContent($languageCode);
|
||||
$modified = max($file, $content);
|
||||
|
||||
if (is_null($format) === true) {
|
||||
return $modified;
|
||||
}
|
||||
|
||||
$handler = $handler ?? $this->kirby()->option('date.handler', 'date');
|
||||
|
||||
return $handler($format, $modified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp of the last modification
|
||||
* of the content file
|
||||
*
|
||||
* @param string|null $languageCode
|
||||
* @return int
|
||||
*/
|
||||
protected function modifiedContent(string $languageCode = null): int
|
||||
{
|
||||
return F::modified($this->contentFile($languageCode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp of the last modification
|
||||
* of the source file
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function modifiedFile(): int
|
||||
{
|
||||
return F::modified($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Page object
|
||||
*
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return is_a($this->parent(), 'Kirby\Cms\Page') === true ? $this->parent() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Panel icon definition
|
||||
*
|
||||
* @internal
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function panelIcon(array $params = null): array
|
||||
{
|
||||
$colorBlue = '#81a2be';
|
||||
$colorPurple = '#b294bb';
|
||||
$colorOrange = '#de935f';
|
||||
$colorGreen = '#a7bd68';
|
||||
$colorAqua = '#8abeb7';
|
||||
$colorYellow = '#f0c674';
|
||||
$colorRed = '#d16464';
|
||||
$colorWhite = '#c5c9c6';
|
||||
|
||||
$types = [
|
||||
'image' => ['color' => $colorOrange, 'type' => 'file-image'],
|
||||
'video' => ['color' => $colorYellow, 'type' => 'file-video'],
|
||||
'document' => ['color' => $colorRed, 'type' => 'file-document'],
|
||||
'audio' => ['color' => $colorAqua, 'type' => 'file-audio'],
|
||||
'code' => ['color' => $colorBlue, 'type' => 'file-code'],
|
||||
'archive' => ['color' => $colorWhite, 'type' => 'file-zip'],
|
||||
];
|
||||
|
||||
$extensions = [
|
||||
'indd' => ['color' => $colorPurple],
|
||||
'xls' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'],
|
||||
'xlsx' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'],
|
||||
'csv' => ['color' => $colorGreen, 'type' => 'file-spreadsheet'],
|
||||
'docx' => ['color' => $colorBlue, 'type' => 'file-word'],
|
||||
'doc' => ['color' => $colorBlue, 'type' => 'file-word'],
|
||||
'rtf' => ['color' => $colorBlue, 'type' => 'file-word'],
|
||||
'mdown' => ['type' => 'file-text'],
|
||||
'md' => ['type' => 'file-text']
|
||||
];
|
||||
|
||||
$definition = array_merge($types[$this->type()] ?? [], $extensions[$this->extension()] ?? []);
|
||||
|
||||
$params['type'] = $definition['type'] ?? 'file';
|
||||
$params['color'] = $definition['color'] ?? $colorWhite;
|
||||
|
||||
return parent::panelIcon($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image file object based on provided query
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $query
|
||||
* @return \Kirby\Cms\File|\Kirby\Cms\Asset|null
|
||||
*/
|
||||
protected function panelImageSource(string $query = null)
|
||||
{
|
||||
if ($query === null && $this->isViewable()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return parent::panelImageSource($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
return 'files/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the response data for file pickers
|
||||
* and file fields
|
||||
*
|
||||
* @param array|null $params
|
||||
* @return array
|
||||
*/
|
||||
public function panelPickerData(array $params = []): array
|
||||
{
|
||||
$image = $this->panelImage($params['image'] ?? []);
|
||||
$icon = $this->panelIcon($image);
|
||||
$uuid = $this->id();
|
||||
|
||||
if (empty($params['model']) === false) {
|
||||
$uuid = $this->parent() === $params['model'] ? $this->filename() : $this->id();
|
||||
$absolute = $this->parent() !== $params['model'];
|
||||
}
|
||||
|
||||
return [
|
||||
'filename' => $this->filename(),
|
||||
'dragText' => $this->dragText('auto', $absolute ?? false),
|
||||
'icon' => $icon,
|
||||
'id' => $this->id(),
|
||||
'image' => $image,
|
||||
'info' => $this->toString($params['info'] ?? false),
|
||||
'link' => $this->panelUrl(true),
|
||||
'text' => $this->toString($params['text'] ?? '{{ file.filename }}'),
|
||||
'type' => $this->type(),
|
||||
'url' => $this->url(),
|
||||
'uuid' => $uuid,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
*/
|
||||
public function panelUrl(bool $relative = false): string
|
||||
{
|
||||
return $this->parent()->panelUrl($relative) . '/' . $this->panelPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Model object
|
||||
*
|
||||
* @return \Kirby\Cms\Model
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent = $this->parent ?? $this->kirby()->site();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent id if a parent exists
|
||||
*
|
||||
* @internal
|
||||
* @return string|null
|
||||
*/
|
||||
public function parentId(): ?string
|
||||
{
|
||||
if ($parent = $this->parent()) {
|
||||
return $parent->id();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of all parent pages
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function parents()
|
||||
{
|
||||
if (is_a($this->parent(), 'Kirby\Cms\Page') === true) {
|
||||
return $this->parent()->parents()->prepend($this->parent()->id(), $this->parent());
|
||||
}
|
||||
|
||||
return new Pages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the permissions object for this file
|
||||
*
|
||||
* @return \Kirby\Cms\FilePermissions
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return new FilePermissions($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute root to the file
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function root(): ?string
|
||||
{
|
||||
return $this->root = $this->root ?? $this->parent()->root() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FileRules class to
|
||||
* validate any important action.
|
||||
*
|
||||
* @return \Kirby\Cms\FileRules
|
||||
*/
|
||||
protected function rules()
|
||||
{
|
||||
return new FileRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Blueprint object
|
||||
*
|
||||
* @param array|null $blueprint
|
||||
* @return self
|
||||
*/
|
||||
protected function setBlueprint(array $blueprint = null)
|
||||
{
|
||||
if ($blueprint !== null) {
|
||||
$blueprint['model'] = $this;
|
||||
$this->blueprint = new FileBlueprint($blueprint);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the filename
|
||||
*
|
||||
* @param string $filename
|
||||
* @return self
|
||||
*/
|
||||
protected function setFilename(string $filename)
|
||||
{
|
||||
$this->filename = $filename;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent model object
|
||||
*
|
||||
* @param \Kirby\Cms\Model $parent
|
||||
* @return self
|
||||
*/
|
||||
protected function setParent(Model $parent = null)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always set the root to null, to invoke
|
||||
* auto root detection
|
||||
*
|
||||
* @param string|null $root
|
||||
* @return self
|
||||
*/
|
||||
protected function setRoot(string $root = null)
|
||||
{
|
||||
$this->root = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
* @return self
|
||||
*/
|
||||
protected function setTemplate(string $template = null)
|
||||
{
|
||||
$this->template = $template;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the url
|
||||
*
|
||||
* @param string $url
|
||||
* @return self
|
||||
*/
|
||||
protected function setUrl(string $url = null)
|
||||
{
|
||||
$this->url = $url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Files collection
|
||||
* @internal
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
protected function siblingsCollection()
|
||||
{
|
||||
return $this->parent()->files();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Site object
|
||||
*
|
||||
* @return \Kirby\Cms\Site
|
||||
*/
|
||||
public function site()
|
||||
{
|
||||
return is_a($this->parent(), 'Kirby\Cms\Site') === true ? $this->parent() : $this->kirby()->site();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final template
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function template(): ?string
|
||||
{
|
||||
return $this->template = $this->template ?? $this->content()->get('template')->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns siblings with the same template
|
||||
*
|
||||
* @param bool $self
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function templateSiblings(bool $self = true)
|
||||
{
|
||||
return $this->siblings($self)->filterBy('template', $this->template());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended info for the array export
|
||||
* by injecting the information from
|
||||
* the asset.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_merge($this->asset()->toArray(), parent::toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function url(): string
|
||||
{
|
||||
return $this->url ?? $this->url = $this->kirby()->component('file::url')($this->kirby(), $this);
|
||||
}
|
||||
}
|
||||
312
kirby/src/Cms/FileActions.php
Normal file
312
kirby/src/Cms/FileActions.php
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* FileActions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait FileActions
|
||||
{
|
||||
/**
|
||||
* Renames the file without touching the extension
|
||||
* The store is used to actually execute this.
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $sanitize
|
||||
* @return self
|
||||
*/
|
||||
public function changeName(string $name, bool $sanitize = true)
|
||||
{
|
||||
if ($sanitize === true) {
|
||||
$name = F::safeName($name);
|
||||
}
|
||||
|
||||
// don't rename if not necessary
|
||||
if ($name === $this->name()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->commit('changeName', ['file' => $this, 'name' => $name], function ($oldFile, $name) {
|
||||
$newFile = $oldFile->clone([
|
||||
'filename' => $name . '.' . $oldFile->extension(),
|
||||
]);
|
||||
|
||||
if ($oldFile->exists() === false) {
|
||||
return $newFile;
|
||||
}
|
||||
|
||||
if ($newFile->exists() === true) {
|
||||
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());
|
||||
|
||||
if ($newFile->kirby()->multilang() === true) {
|
||||
foreach ($newFile->translations() as $translation) {
|
||||
$translationCode = $translation->code();
|
||||
|
||||
// rename the content file
|
||||
F::move($oldFile->contentFile($translationCode), $newFile->contentFile($translationCode));
|
||||
}
|
||||
} else {
|
||||
// rename the content file
|
||||
F::move($oldFile->contentFile(), $newFile->contentFile());
|
||||
}
|
||||
|
||||
|
||||
return $newFile;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the file's sorting number in the meta file
|
||||
*
|
||||
* @param int $sort
|
||||
* @return self
|
||||
*/
|
||||
public function changeSort(int $sort)
|
||||
{
|
||||
return $this->commit('changeSort', ['file' => $this, 'position' => $sort], function ($file, $sort) {
|
||||
return $file->save(['sort' => $sort]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits a file action, by following these steps
|
||||
*
|
||||
* 1. checks the action rules
|
||||
* 2. sends the before hook
|
||||
* 3. commits the store action
|
||||
* 4. sends the after hook
|
||||
* 5. returns the result
|
||||
*
|
||||
* @param string $action
|
||||
* @param array $arguments
|
||||
* @param Closure $callback
|
||||
* @return mixed
|
||||
*/
|
||||
protected function commit(string $action, array $arguments, Closure $callback)
|
||||
{
|
||||
$old = $this->hardcopy();
|
||||
$kirby = $this->kirby();
|
||||
$argumentValues = array_values($arguments);
|
||||
|
||||
$this->rules()->$action(...$argumentValues);
|
||||
$kirby->trigger('file.' . $action . ':before', $arguments);
|
||||
|
||||
$result = $callback(...$argumentValues);
|
||||
|
||||
if ($action === 'create') {
|
||||
$argumentsAfter = ['file' => $result];
|
||||
} elseif ($action === 'delete') {
|
||||
$argumentsAfter = ['status' => $result, 'file' => $old];
|
||||
} else {
|
||||
$argumentsAfter = ['newFile' => $result, 'oldFile' => $old];
|
||||
}
|
||||
$kirby->trigger('file.' . $action . ':after', $argumentsAfter);
|
||||
|
||||
$kirby->cache('pages')->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the file to the given page
|
||||
*
|
||||
* @param \Kirby\Cms\Page $page
|
||||
* @return \Kirby\Cms\File
|
||||
*/
|
||||
public function copy(Page $page)
|
||||
{
|
||||
F::copy($this->root(), $page->root() . '/' . $this->filename());
|
||||
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
foreach ($this->kirby()->languages() as $language) {
|
||||
$contentFile = $this->contentFile($language->code());
|
||||
F::copy($contentFile, $page->root() . '/' . basename($contentFile));
|
||||
}
|
||||
} else {
|
||||
$contentFile = $this->contentFile();
|
||||
F::copy($contentFile, $page->root() . '/' . basename($contentFile));
|
||||
}
|
||||
|
||||
return $page->clone()->file($this->filename());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file on disk and returns the
|
||||
* File object. The store is used to handle file
|
||||
* writing, so it can be replaced by any other
|
||||
* way of generating files.
|
||||
*
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public static function create(array $props)
|
||||
{
|
||||
if (isset($props['source'], $props['parent']) === false) {
|
||||
throw new InvalidArgumentException('Please provide the "source" and "parent" props for the File');
|
||||
}
|
||||
|
||||
// prefer the filename from the props
|
||||
$props['filename'] = F::safeName($props['filename'] ?? basename($props['source']));
|
||||
|
||||
$props['model'] = strtolower($props['template'] ?? 'default');
|
||||
|
||||
// create the basic file and a test upload object
|
||||
$file = static::factory($props);
|
||||
$upload = new Image($props['source']);
|
||||
|
||||
// create a form for the file
|
||||
$form = Form::for($file, [
|
||||
'values' => $props['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
|
||||
$file->unpublish();
|
||||
|
||||
// overwrite the original
|
||||
if (F::copy($upload->root(), $file->root(), true) !== true) {
|
||||
throw new LogicException('The file could not be created');
|
||||
}
|
||||
|
||||
// always create pages in the default language
|
||||
if ($file->kirby()->multilang() === true) {
|
||||
$languageCode = $file->kirby()->defaultLanguage()->code();
|
||||
} else {
|
||||
$languageCode = null;
|
||||
}
|
||||
|
||||
// store the content if necessary
|
||||
$file->save($file->content()->toArray(), $languageCode);
|
||||
|
||||
// add the file to the list of siblings
|
||||
$file->siblings()->append($file->id(), $file);
|
||||
|
||||
// return a fresh clone
|
||||
return $file->clone();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file. The store is used to
|
||||
* manipulate the filesystem or whatever you prefer.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
return $this->commit('delete', ['file' => $this], function ($file) {
|
||||
|
||||
// remove all versions in the media folder
|
||||
$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()));
|
||||
}
|
||||
} else {
|
||||
F::remove($file->contentFile());
|
||||
}
|
||||
|
||||
F::remove($file->root());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the file to the public media folder
|
||||
* if it's not already there.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function publish()
|
||||
{
|
||||
Media::publish($this, $this->mediaRoot());
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `File::changeName()` instead
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $sanitize
|
||||
* @return self
|
||||
*/
|
||||
public function rename(string $name, bool $sanitize = true)
|
||||
{
|
||||
deprecated('$file->rename() is deprecated, use $file->changeName() instead. $file->rename() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->changeName($name, $sanitize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the file. The source must
|
||||
* be an absolute path to a file or a Url.
|
||||
* The store handles the replacement so it
|
||||
* finally decides what it will support as
|
||||
* source.
|
||||
*
|
||||
* @param string $source
|
||||
* @return self
|
||||
*/
|
||||
public function replace(string $source)
|
||||
{
|
||||
return $this->commit('replace', ['file' => $this, 'upload' => new Image($source)], function ($file, $upload) {
|
||||
|
||||
// delete all public versions
|
||||
$file->unpublish();
|
||||
|
||||
// overwrite the original
|
||||
if (F::copy($upload->root(), $file->root(), true) !== true) {
|
||||
throw new LogicException('The file could not be created');
|
||||
}
|
||||
|
||||
// return a fresh clone
|
||||
return $file->clone();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all public versions of this file
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function unpublish()
|
||||
{
|
||||
Media::unpublish($this->parent()->mediaRoot(), $this);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
79
kirby/src/Cms/FileBlueprint.php
Normal file
79
kirby/src/Cms/FileBlueprint.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of the basic blueprint class
|
||||
* to handle all blueprints for files.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class FileBlueprint extends Blueprint
|
||||
{
|
||||
public function __construct(array $props)
|
||||
{
|
||||
parent::__construct($props);
|
||||
|
||||
// normalize all available page options
|
||||
$this->props['options'] = $this->normalizeOptions(
|
||||
$this->props['options'] ?? true,
|
||||
// defaults
|
||||
[
|
||||
'changeName' => null,
|
||||
'create' => null,
|
||||
'delete' => null,
|
||||
'read' => null,
|
||||
'replace' => null,
|
||||
'update' => null,
|
||||
]
|
||||
);
|
||||
|
||||
// normalize the accept settings
|
||||
$this->props['accept'] = $this->normalizeAccept($this->props['accept'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function accept(): array
|
||||
{
|
||||
return $this->props['accept'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $accept
|
||||
* @return array
|
||||
*/
|
||||
protected function normalizeAccept($accept = null): array
|
||||
{
|
||||
if (is_string($accept) === true) {
|
||||
$accept = [
|
||||
'mime' => $accept
|
||||
];
|
||||
}
|
||||
|
||||
// accept anything
|
||||
if (empty($accept) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$accept = array_change_key_case($accept);
|
||||
|
||||
$defaults = [
|
||||
'mime' => null,
|
||||
'maxheight' => null,
|
||||
'maxsize' => null,
|
||||
'maxwidth' => null,
|
||||
'minheight' => null,
|
||||
'minsize' => null,
|
||||
'minwidth' => null,
|
||||
'orientation' => null
|
||||
];
|
||||
|
||||
return array_merge($defaults, $accept);
|
||||
}
|
||||
}
|
||||
247
kirby/src/Cms/FileFoundation.php
Normal file
247
kirby/src/Cms/FileFoundation.php
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* Foundation for all file objects
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait FileFoundation
|
||||
{
|
||||
protected $asset;
|
||||
protected $root;
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Magic caller for asset methods
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
// public property access
|
||||
if (isset($this->$method) === true) {
|
||||
return $this->$method;
|
||||
}
|
||||
|
||||
// asset method proxy
|
||||
if (method_exists($this->asset(), $method)) {
|
||||
return $this->asset()->$method(...$arguments);
|
||||
}
|
||||
|
||||
throw new BadMethodCallException('The method: "' . $method . '" does not exist');
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor sets all file properties
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the file object to a string
|
||||
* In case of an image, it will create an image tag
|
||||
* Otherwise it will return the url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->type() === 'image') {
|
||||
return $this->html();
|
||||
}
|
||||
|
||||
return $this->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Image object
|
||||
*
|
||||
* @return \Kirby\Image\Image
|
||||
*/
|
||||
public function asset()
|
||||
{
|
||||
return $this->asset = $this->asset ?? new Image($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file exists on disk
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->root()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file extension
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function extension(): string
|
||||
{
|
||||
return F::extension($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the file to html
|
||||
*
|
||||
* @param array $attr
|
||||
* @return string
|
||||
*/
|
||||
public function html(array $attr = []): string
|
||||
{
|
||||
if ($this->type() === 'image') {
|
||||
return Html::img($this->url(), array_merge(['alt' => $this->alt()], $attr));
|
||||
} else {
|
||||
return Html::a($this->url(), $attr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is a resizable image
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isResizable(): bool
|
||||
{
|
||||
$resizable = [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'gif',
|
||||
'png',
|
||||
'webp'
|
||||
];
|
||||
|
||||
return in_array($this->extension(), $resizable) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a preview can be displayed for the file
|
||||
* in the panel or in the frontend
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isViewable(): bool
|
||||
{
|
||||
$viewable = [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'gif',
|
||||
'png',
|
||||
'svg',
|
||||
'webp'
|
||||
];
|
||||
|
||||
return in_array($this->extension(), $viewable) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app instance
|
||||
*
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
{
|
||||
return App::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's last modification time.
|
||||
*
|
||||
* @param string $format
|
||||
* @param string|null $handler date or strftime
|
||||
* @return mixed
|
||||
*/
|
||||
public function modified(string $format = null, string $handler = null)
|
||||
{
|
||||
return F::modified($this->root(), $format, $handler ?? $this->kirby()->option('date.handler', 'date'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file root
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function root(): ?string
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the root
|
||||
*
|
||||
* @param string $root
|
||||
* @return self
|
||||
*/
|
||||
protected function setRoot(string $root = null)
|
||||
{
|
||||
$this->root = $root;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the file url
|
||||
*
|
||||
* @param string $url
|
||||
* @return self
|
||||
*/
|
||||
protected function setUrl(string $url)
|
||||
{
|
||||
$this->url = $url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = array_merge($this->asset()->toArray(), [
|
||||
'isResizable' => $this->isResizable(),
|
||||
'url' => $this->url(),
|
||||
]);
|
||||
|
||||
ksort($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file type
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function type(): ?string
|
||||
{
|
||||
return F::type($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url for the file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function url(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
}
|
||||
200
kirby/src/Cms/FileModifications.php
Normal file
200
kirby/src/Cms/FileModifications.php
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Resizing, blurring etc.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait FileModifications
|
||||
{
|
||||
/**
|
||||
* Blurs the image by the given amount of pixels
|
||||
*
|
||||
* @param bool $pixels
|
||||
* @return \Kirby\Cms\FileVersion|\Kirby\Cms\File
|
||||
*/
|
||||
public function blur($pixels = true)
|
||||
{
|
||||
return $this->thumb(['blur' => $pixels]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the image to black and white
|
||||
*
|
||||
* @return \Kirby\Cms\FileVersion|\Kirby\Cms\File
|
||||
*/
|
||||
public function bw()
|
||||
{
|
||||
return $this->thumb(['grayscale' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crops the image by the given width and height
|
||||
*
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param string|array $options
|
||||
* @return \Kirby\Cms\FileVersion|\Kirby\Cms\File
|
||||
*/
|
||||
public function crop(int $width, int $height = null, $options = null)
|
||||
{
|
||||
$quality = null;
|
||||
$crop = 'center';
|
||||
|
||||
if (is_int($options) === true) {
|
||||
$quality = $options;
|
||||
} elseif (is_string($options)) {
|
||||
$crop = $options;
|
||||
} elseif (is_a($options, 'Kirby\Cms\Field') === true) {
|
||||
$crop = $options->value();
|
||||
} elseif (is_array($options)) {
|
||||
$quality = $options['quality'] ?? $quality;
|
||||
$crop = $options['crop'] ?? $crop;
|
||||
}
|
||||
|
||||
return $this->thumb([
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'quality' => $quality,
|
||||
'crop' => $crop
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for File::bw()
|
||||
*
|
||||
* @return \Kirby\Cms\FileVersion|\Kirby\Cms\File
|
||||
*/
|
||||
public function grayscale()
|
||||
{
|
||||
return $this->thumb(['grayscale' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for File::bw()
|
||||
*
|
||||
* @return \Kirby\Cms\FileVersion|\Kirby\Cms\File
|
||||
*/
|
||||
public function greyscale()
|
||||
{
|
||||
return $this->thumb(['grayscale' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JPEG compression quality
|
||||
*
|
||||
* @param int $quality
|
||||
* @return \Kirby\Cms\FileVersion|\Kirby\Cms\File
|
||||
*/
|
||||
public function quality(int $quality)
|
||||
{
|
||||
return $this->thumb(['quality' => $quality]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the file with the given width and height
|
||||
* while keeping the aspect ratio.
|
||||
*
|
||||
* @param int $width
|
||||
* @param int $height
|
||||
* @param int $quality
|
||||
* @return \Kirby\Cms\FileVersion|\Kirby\Cms\File
|
||||
*/
|
||||
public function resize(int $width = null, int $height = null, int $quality = null)
|
||||
{
|
||||
return $this->thumb([
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'quality' => $quality
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a srcset definition for the given sizes
|
||||
* Sizes can be defined as a simple array. They can
|
||||
* also be set up in the config with the thumbs.srcsets option.
|
||||
* @since 3.1.0
|
||||
*
|
||||
* @param array|string $sizes
|
||||
* @return string|null
|
||||
*/
|
||||
public function srcset($sizes = null): ?string
|
||||
{
|
||||
if (empty($sizes) === true) {
|
||||
$sizes = $this->kirby()->option('thumbs.srcsets.default', []);
|
||||
}
|
||||
|
||||
if (is_string($sizes) === true) {
|
||||
$sizes = $this->kirby()->option('thumbs.srcsets.' . $sizes, []);
|
||||
}
|
||||
|
||||
if (is_array($sizes) === false || empty($sizes) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$set = [];
|
||||
|
||||
foreach ($sizes as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$options = $value;
|
||||
$condition = $key;
|
||||
} elseif (is_string($value) === true) {
|
||||
$options = [
|
||||
'width' => $key
|
||||
];
|
||||
$condition = $value;
|
||||
} else {
|
||||
$options = [
|
||||
'width' => $value
|
||||
];
|
||||
$condition = $value . 'w';
|
||||
}
|
||||
|
||||
$set[] = $this->thumb($options)->url() . ' ' . $condition;
|
||||
}
|
||||
|
||||
return implode(', ', $set);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a modified version of images
|
||||
* The media manager takes care of generating
|
||||
* those modified versions and putting them
|
||||
* in the right place. This is normally the
|
||||
* `/media` folder of your installation, but
|
||||
* could potentially also be a CDN or any other
|
||||
* place.
|
||||
*
|
||||
* @param array|null|string $options
|
||||
* @return \Kirby\Cms\FileVersion|\Kirby\Cms\File
|
||||
*/
|
||||
public function thumb($options = null)
|
||||
{
|
||||
// thumb presets
|
||||
if (empty($options) === true) {
|
||||
$options = $this->kirby()->option('thumbs.presets.default');
|
||||
} elseif (is_string($options) === true) {
|
||||
$options = $this->kirby()->option('thumbs.presets.' . $options);
|
||||
}
|
||||
|
||||
if (empty($options) === true || is_array($options) === false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$result = $this->kirby()->component('file::version')($this->kirby(), $this, $options);
|
||||
|
||||
if (is_a($result, 'Kirby\Cms\FileVersion') === false && is_a($result, 'Kirby\Cms\File') === false) {
|
||||
throw new InvalidArgumentException('The file::version component must return a File or FileVersion object');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
17
kirby/src/Cms/FilePermissions.php
Normal file
17
kirby/src/Cms/FilePermissions.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* FilePermissions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class FilePermissions extends ModelPermissions
|
||||
{
|
||||
protected $category = 'files';
|
||||
}
|
||||
73
kirby/src/Cms/FilePicker.php
Normal file
73
kirby/src/Cms/FilePicker.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* The FilePicker class helps to
|
||||
* fetch the right files for the API calls
|
||||
* for the file picker component in the panel.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class FilePicker extends Picker
|
||||
{
|
||||
/**
|
||||
* Extends the basic defaults
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function defaults(): array
|
||||
{
|
||||
$defaults = parent::defaults();
|
||||
$defaults['text'] = '{{ file.filename }}';
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all files for the picker
|
||||
*
|
||||
* @return \Kirby\Cms\Files|null
|
||||
*/
|
||||
public function items()
|
||||
{
|
||||
$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';
|
||||
}
|
||||
|
||||
// fetch all files for the picker
|
||||
$files = $model->query($query);
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// search
|
||||
$files = $this->search($files);
|
||||
|
||||
// paginate
|
||||
return $this->paginate($files);
|
||||
}
|
||||
}
|
||||
208
kirby/src/Cms/FileRules.php
Normal file
208
kirby/src/Cms/FileRules.php
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
/**
|
||||
* Validators for all file actions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class FileRules
|
||||
{
|
||||
public static function changeName(File $file, string $name): bool
|
||||
{
|
||||
if ($file->permissions()->changeName() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'file.changeName.permission',
|
||||
'data' => ['filename' => $file->filename()]
|
||||
]);
|
||||
}
|
||||
|
||||
$parent = $file->parent();
|
||||
$duplicate = $parent->files()->not($file)->findBy('filename', $name . '.' . $file->extension());
|
||||
|
||||
if ($duplicate) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'file.duplicate',
|
||||
'data' => ['filename' => $duplicate->filename()]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function changeSort(File $file, int $sort): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function create(File $file, Image $upload): bool
|
||||
{
|
||||
if ($file->exists() === true) {
|
||||
throw new LogicException('The file exists and cannot be overwritten');
|
||||
}
|
||||
|
||||
if ($file->permissions()->create() !== true) {
|
||||
throw new PermissionException('The file cannot be created');
|
||||
}
|
||||
|
||||
static::validExtension($file, $file->extension());
|
||||
static::validMime($file, $upload->mime());
|
||||
static::validFilename($file, $file->filename());
|
||||
|
||||
$upload->match($file->blueprint()->accept());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function delete(File $file): bool
|
||||
{
|
||||
if ($file->permissions()->delete() !== true) {
|
||||
throw new PermissionException('The file cannot be deleted');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function replace(File $file, Image $upload): bool
|
||||
{
|
||||
if ($file->permissions()->replace() !== true) {
|
||||
throw new PermissionException('The file cannot be replaced');
|
||||
}
|
||||
|
||||
static::validMime($file, $upload->mime());
|
||||
|
||||
|
||||
if (
|
||||
(string)$upload->mime() !== (string)$file->mime() &&
|
||||
(string)$upload->extension() !== (string)$file->extension()
|
||||
) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.mime.differs',
|
||||
'data' => ['mime' => $file->mime()]
|
||||
]);
|
||||
}
|
||||
|
||||
$upload->match($file->blueprint()->accept());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function update(File $file, array $content = []): bool
|
||||
{
|
||||
if ($file->permissions()->update() !== true) {
|
||||
throw new PermissionException('The file cannot be updated');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validExtension(File $file, string $extension): bool
|
||||
{
|
||||
// make it easier to compare the extension
|
||||
$extension = strtolower($extension);
|
||||
|
||||
if (empty($extension)) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.extension.missing',
|
||||
'data' => ['filename' => $file->filename()]
|
||||
]);
|
||||
}
|
||||
|
||||
if (V::in($extension, ['php', 'html', 'htm', 'exe', App::instance()->contentExtension()])) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.extension.forbidden',
|
||||
'data' => ['extension' => $extension]
|
||||
]);
|
||||
}
|
||||
|
||||
if (Str::contains($extension, 'php')) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.type.forbidden',
|
||||
'data' => ['type' => 'PHP']
|
||||
]);
|
||||
}
|
||||
|
||||
if (Str::contains($extension, 'htm')) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.type.forbidden',
|
||||
'data' => ['type' => 'HTML']
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validFilename(File $file, string $filename)
|
||||
{
|
||||
|
||||
// make it easier to compare the filename
|
||||
$filename = strtolower($filename);
|
||||
|
||||
// check for missing filenames
|
||||
if (empty($filename)) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.name.missing'
|
||||
]);
|
||||
}
|
||||
|
||||
// Block htaccess files
|
||||
if (Str::startsWith($filename, '.ht')) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.type.forbidden',
|
||||
'data' => ['type' => 'Apache config']
|
||||
]);
|
||||
}
|
||||
|
||||
// Block invisible files
|
||||
if (Str::startsWith($filename, '.')) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.type.forbidden',
|
||||
'data' => ['type' => 'invisible']
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validMime(File $file, string $mime = null)
|
||||
{
|
||||
// make it easier to compare the mime
|
||||
$mime = strtolower($mime);
|
||||
|
||||
if (empty($mime)) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.mime.missing',
|
||||
'data' => ['filename' => $file->filename()]
|
||||
]);
|
||||
}
|
||||
|
||||
if (Str::contains($mime, 'php')) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.type.forbidden',
|
||||
'data' => ['type' => 'PHP']
|
||||
]);
|
||||
}
|
||||
|
||||
if (V::in($mime, ['text/html', 'application/x-msdownload'])) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'file.mime.forbidden',
|
||||
'data' => ['mime' => $mime]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
102
kirby/src/Cms/FileVersion.php
Normal file
102
kirby/src/Cms/FileVersion.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
||||
/**
|
||||
* FileVersion
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class FileVersion
|
||||
{
|
||||
use FileFoundation {
|
||||
toArray as parentToArray;
|
||||
}
|
||||
use Properties;
|
||||
|
||||
protected $modifications;
|
||||
protected $original;
|
||||
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
// public property access
|
||||
if (isset($this->$method) === true) {
|
||||
return $this->$method;
|
||||
}
|
||||
|
||||
// asset method proxy
|
||||
if (method_exists($this->asset(), $method)) {
|
||||
if ($this->exists() === false) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $this->asset()->$method(...$arguments);
|
||||
}
|
||||
|
||||
if (is_a($this->original(), 'Kirby\Cms\File') === true) {
|
||||
// content fields
|
||||
return $this->original()->content()->get($method, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return dirname($this->original()->id()) . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
{
|
||||
return $this->original()->kirby();
|
||||
}
|
||||
|
||||
public function modifications(): array
|
||||
{
|
||||
return $this->modifications ?? [];
|
||||
}
|
||||
|
||||
public function original()
|
||||
{
|
||||
return $this->original;
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->kirby()->thumb($this->original()->root(), $this->root(), $this->modifications());
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setModifications(array $modifications = null)
|
||||
{
|
||||
$this->modifications = $modifications;
|
||||
}
|
||||
|
||||
protected function setOriginal($original)
|
||||
{
|
||||
$this->original = $original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = array_merge($this->parentToArray(), [
|
||||
'modifications' => $this->modifications(),
|
||||
]);
|
||||
|
||||
ksort($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
303
kirby/src/Cms/Filename.php
Normal file
303
kirby/src/Cms/Filename.php
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Filename class handles complex
|
||||
* mapping of file attributes (i.e for thumbnails)
|
||||
* into human readable filenames.
|
||||
*
|
||||
* ```php
|
||||
* $filename = new Filename('some-file.jpg', '{{ name }}-{{ attributes }}.{{ extension }}', [
|
||||
* 'crop' => 'top left',
|
||||
* 'width' => 300,
|
||||
* 'height' => 200
|
||||
* 'quality' => 80
|
||||
* ]);
|
||||
*
|
||||
* echo $filename->toString();
|
||||
* // result: some-file-300x200-crop-top-left-q80.jpg
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Filename
|
||||
{
|
||||
/**
|
||||
* List of all applicable attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attributes;
|
||||
|
||||
/**
|
||||
* The sanitized file extension
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $extension;
|
||||
|
||||
/**
|
||||
* The source original filename
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $filename;
|
||||
|
||||
/**
|
||||
* The sanitized file name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The template for the final name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $template;
|
||||
|
||||
/**
|
||||
* Creates a new Filename object
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $template
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct(string $filename, string $template, array $attributes = [])
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->template = $template;
|
||||
$this->attributes = $attributes;
|
||||
$this->extension = $this->sanitizeExtension(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
$this->name = $this->sanitizeName(pathinfo($filename, PATHINFO_FILENAME));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the entire object to a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all processed attributes
|
||||
* to an array. The array keys are already
|
||||
* the shortened versions for the filename
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function attributesToArray(): array
|
||||
{
|
||||
$array = [
|
||||
'dimensions' => implode('x', $this->dimensions()),
|
||||
'crop' => $this->crop(),
|
||||
'blur' => $this->blur(),
|
||||
'bw' => $this->grayscale(),
|
||||
'q' => $this->quality(),
|
||||
];
|
||||
|
||||
$array = array_filter($array, function ($item) {
|
||||
return $item !== null && $item !== false && $item !== '';
|
||||
});
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all processed attributes
|
||||
* to a string, that can be used in the
|
||||
* new filename
|
||||
*
|
||||
* @param string $prefix The prefix will be used in the filename creation
|
||||
* @return string
|
||||
*/
|
||||
public function attributesToString(string $prefix = null): string
|
||||
{
|
||||
$array = $this->attributesToArray();
|
||||
$result = [];
|
||||
|
||||
foreach ($array as $key => $value) {
|
||||
if ($value === true) {
|
||||
$value = '';
|
||||
}
|
||||
|
||||
switch ($key) {
|
||||
case 'dimensions':
|
||||
$result[] = $value;
|
||||
break;
|
||||
case 'crop':
|
||||
$result[] = ($value === 'center') ? null : $key . '-' . $value;
|
||||
break;
|
||||
default:
|
||||
$result[] = $key . $value;
|
||||
}
|
||||
}
|
||||
|
||||
$result = array_filter($result);
|
||||
$attributes = implode('-', $result);
|
||||
|
||||
if (empty($attributes) === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $prefix . $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the blur option value
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public function blur()
|
||||
{
|
||||
$value = $this->attributes['blur'] ?? false;
|
||||
|
||||
if ($value === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the crop option value
|
||||
*
|
||||
* @return false|string
|
||||
*/
|
||||
public function crop()
|
||||
{
|
||||
// get the crop value
|
||||
$crop = $this->attributes['crop'] ?? false;
|
||||
|
||||
if ($crop === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Str::slug($crop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a normalized array
|
||||
* with width and height values
|
||||
* if available
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function dimensions()
|
||||
{
|
||||
if (empty($this->attributes['width']) === true && empty($this->attributes['height']) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'width' => $this->attributes['width'] ?? null,
|
||||
'height' => $this->attributes['height'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sanitized extension
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function extension(): string
|
||||
{
|
||||
return $this->extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the grayscale option value
|
||||
* and also the available ways to write
|
||||
* the option. You can use `grayscale`,
|
||||
* `greyscale` or simply `bw`. The function
|
||||
* will always return `grayscale`
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function grayscale(): bool
|
||||
{
|
||||
// normalize options
|
||||
$value = $this->attributes['grayscale'] ?? $this->attributes['greyscale'] ?? $this->attributes['bw'] ?? false;
|
||||
|
||||
// turn anything into boolean
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename without extension
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the quality option value
|
||||
*
|
||||
* @return false|int
|
||||
*/
|
||||
public function quality()
|
||||
{
|
||||
$value = $this->attributes['quality'] ?? false;
|
||||
|
||||
if ($value === false || $value === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int)$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the file extension.
|
||||
* The extension will be converted
|
||||
* to lowercase and `jpeg` will be
|
||||
* replaced with `jpg`
|
||||
*
|
||||
* @param string $extension
|
||||
* @return string
|
||||
*/
|
||||
protected function sanitizeExtension(string $extension): string
|
||||
{
|
||||
$extension = strtolower($extension);
|
||||
$extension = str_replace('jpeg', 'jpg', $extension);
|
||||
return $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the name with Kirby's
|
||||
* Str::slug function
|
||||
*
|
||||
* @param string $name
|
||||
* @return string
|
||||
*/
|
||||
protected function sanitizeName(string $name): string
|
||||
{
|
||||
return Str::slug($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the converted filename as string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return Str::template($this->template, [
|
||||
'name' => $this->name(),
|
||||
'attributes' => $this->attributesToString('-'),
|
||||
'extension' => $this->extension()
|
||||
], '');
|
||||
}
|
||||
}
|
||||
138
kirby/src/Cms/Files.php
Normal file
138
kirby/src/Cms/Files.php
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The `$files` object extends the general
|
||||
* `Collection` class and refers to a
|
||||
* collection of files, i.e. images, documents
|
||||
* etc. Files can be filtered, searched,
|
||||
* converted, modified or evaluated with the
|
||||
* following methods:
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Files extends Collection
|
||||
{
|
||||
/**
|
||||
* All registered files methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $methods = [];
|
||||
|
||||
/**
|
||||
* Adds a single file or
|
||||
* an entire second collection to the
|
||||
* current collection
|
||||
*
|
||||
* @param mixed $object
|
||||
* @return self
|
||||
*/
|
||||
public function add($object)
|
||||
{
|
||||
// add a page collection
|
||||
if (is_a($object, static::class) === true) {
|
||||
$this->data = array_merge($this->data, $object->data);
|
||||
|
||||
// add a file by id
|
||||
} 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) {
|
||||
$this->__set($object->id(), $object);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort all given files by the
|
||||
* order in the array
|
||||
*
|
||||
* @param array $files List of file ids
|
||||
* @param int $offset Sorting offset
|
||||
* @return self
|
||||
*/
|
||||
public function changeSort(array $files, int $offset = 0)
|
||||
{
|
||||
foreach ($files as $filename) {
|
||||
if ($file = $this->get($filename)) {
|
||||
$offset++;
|
||||
$file->changeSort($offset);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a files collection from an array of props
|
||||
*
|
||||
* @param array $files
|
||||
* @param \Kirby\Cms\Model $parent
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function factory(array $files, Model $parent)
|
||||
{
|
||||
$collection = new static([], $parent);
|
||||
$kirby = $parent->kirby();
|
||||
|
||||
foreach ($files as $props) {
|
||||
$props['collection'] = $collection;
|
||||
$props['kirby'] = $kirby;
|
||||
$props['parent'] = $parent;
|
||||
|
||||
$file = File::factory($props);
|
||||
|
||||
$collection->data[$file->id()] = $file;
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a file by id/filename
|
||||
*
|
||||
* @param string $id
|
||||
* @return \Kirby\Cms\File|null
|
||||
*/
|
||||
public function findById(string $id)
|
||||
{
|
||||
return $this->get(ltrim($this->parent->id() . '/' . $id, '/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for FilesFinder::findById() which is
|
||||
* used internally in the Files collection to
|
||||
* map the get method correctly.
|
||||
*
|
||||
* @param string $key
|
||||
* @return \Kirby\Cms\File|null
|
||||
*/
|
||||
public function findByKey(string $key)
|
||||
{
|
||||
return $this->findById($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all files by the given template
|
||||
*
|
||||
* @param null|string|array $template
|
||||
* @return self
|
||||
*/
|
||||
public function template($template)
|
||||
{
|
||||
if (empty($template) === true) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->filterBy('template', is_array($template) ? 'in' : '==', $template);
|
||||
}
|
||||
}
|
||||
87
kirby/src/Cms/Form.php
Normal file
87
kirby/src/Cms/Form.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Form\Form as BaseForm;
|
||||
|
||||
/**
|
||||
* Extension of `Kirby\Form\Form` that introduces
|
||||
* a Form::for method that creates a proper form
|
||||
* definition for any Cms Model.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Form extends BaseForm
|
||||
{
|
||||
protected $errors;
|
||||
protected $fields;
|
||||
protected $values = [];
|
||||
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$kirby = App::instance();
|
||||
|
||||
if ($kirby->multilang() === true) {
|
||||
$fields = $props['fields'] ?? [];
|
||||
$languageCode = $props['language'] ?? $kirby->language()->code();
|
||||
$isDefaultLanguage = $languageCode === $kirby->defaultLanguage()->code();
|
||||
|
||||
foreach ($fields as $fieldName => $fieldProps) {
|
||||
// switch untranslatable fields to readonly
|
||||
if (($fieldProps['translate'] ?? true) === false && $isDefaultLanguage === false) {
|
||||
$fields[$fieldName]['unset'] = true;
|
||||
$fields[$fieldName]['disabled'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$props['fields'] = $fields;
|
||||
}
|
||||
|
||||
parent::__construct($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\Model $model
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public static function for(Model $model, array $props = [])
|
||||
{
|
||||
// get the original model data
|
||||
$original = $model->content($props['language'] ?? null)->toArray();
|
||||
$values = $props['values'] ?? [];
|
||||
|
||||
// convert closures to values
|
||||
foreach ($values as $key => $value) {
|
||||
if (is_a($value, 'Closure') === true) {
|
||||
$values[$key] = $value($original[$key] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
// set a few defaults
|
||||
$props['values'] = array_merge($original, $values);
|
||||
$props['fields'] = $props['fields'] ?? [];
|
||||
$props['model'] = $model;
|
||||
|
||||
// search for the blueprint
|
||||
if (method_exists($model, 'blueprint') === true && $blueprint = $model->blueprint()) {
|
||||
$props['fields'] = $blueprint->fields();
|
||||
}
|
||||
|
||||
$ignoreDisabled = $props['ignoreDisabled'] ?? false;
|
||||
|
||||
// REFACTOR: this could be more elegant
|
||||
if ($ignoreDisabled === true) {
|
||||
$props['fields'] = array_map(function ($field) {
|
||||
$field['disabled'] = false;
|
||||
return $field;
|
||||
}, $props['fields']);
|
||||
}
|
||||
|
||||
return new static($props);
|
||||
}
|
||||
}
|
||||
263
kirby/src/Cms/HasChildren.php
Normal file
263
kirby/src/Cms/HasChildren.php
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* HasChildren
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait HasChildren
|
||||
{
|
||||
/**
|
||||
* The Pages collection
|
||||
*
|
||||
* @var \Kirby\Cms\Pages
|
||||
*/
|
||||
public $children;
|
||||
|
||||
/**
|
||||
* The list of available drafts
|
||||
*
|
||||
* @var \Kirby\Cms\Pages
|
||||
*/
|
||||
public $drafts;
|
||||
|
||||
/**
|
||||
* Returns the Pages collection
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
if (is_a($this->children, 'Kirby\Cms\Pages') === true) {
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
return $this->children = Pages::factory($this->inventory()['children'], $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all children and drafts at the same time
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function childrenAndDrafts()
|
||||
{
|
||||
return $this->children()->merge($this->drafts());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of ids for the model's
|
||||
* toArray method
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function convertChildrenToArray(): array
|
||||
{
|
||||
return $this->children()->keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a child draft by id
|
||||
*
|
||||
* @param string $path
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function draft(string $path)
|
||||
{
|
||||
$path = str_replace('_drafts/', '', $path);
|
||||
|
||||
if (Str::contains($path, '/') === false) {
|
||||
return $this->drafts()->find($path);
|
||||
}
|
||||
|
||||
$parts = explode('/', $path);
|
||||
$parent = $this;
|
||||
|
||||
foreach ($parts as $slug) {
|
||||
if ($page = $parent->find($slug)) {
|
||||
$parent = $page;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($draft = $parent->drafts()->find($slug)) {
|
||||
$parent = $draft;
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all drafts of the model
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function drafts()
|
||||
{
|
||||
if (is_a($this->drafts, 'Kirby\Cms\Pages') === true) {
|
||||
return $this->drafts;
|
||||
}
|
||||
|
||||
$kirby = $this->kirby();
|
||||
|
||||
// create the inventory for all drafts
|
||||
$inventory = Dir::inventory(
|
||||
$this->root() . '/_drafts',
|
||||
$kirby->contentExtension(),
|
||||
$kirby->contentIgnore(),
|
||||
$kirby->multilang()
|
||||
);
|
||||
|
||||
return $this->drafts = Pages::factory($inventory['children'], $this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds one or multiple children by id
|
||||
*
|
||||
* @param string ...$arguments
|
||||
* @return \Kirby\Cms\Page|\Kirby\Cms\Pages|null
|
||||
*/
|
||||
public function find(...$arguments)
|
||||
{
|
||||
return $this->children()->find(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single page or draft
|
||||
*
|
||||
* @param string $path
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function findPageOrDraft(string $path)
|
||||
{
|
||||
return $this->children()->find($path) ?? $this->drafts()->find($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of all children of children
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function grandChildren()
|
||||
{
|
||||
return $this->children()->children();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model has any children
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return $this->children()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model has any drafts
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDrafts(): bool
|
||||
{
|
||||
return $this->drafts()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasUnlistedChildren()` instead
|
||||
* @return bool
|
||||
*/
|
||||
public function hasInvisibleChildren(): bool
|
||||
{
|
||||
deprecated('$page->hasInvisibleChildren() is deprecated, use $page->hasUnlistedChildren() instead. $page->hasInvisibleChildren() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasUnlistedChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page has any listed children
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasListedChildren(): bool
|
||||
{
|
||||
return $this->children()->listed()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page has any unlisted children
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUnlistedChildren(): bool
|
||||
{
|
||||
return $this->children()->unlisted()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasListedChildren()` instead
|
||||
* @return bool
|
||||
*/
|
||||
public function hasVisibleChildren(): bool
|
||||
{
|
||||
deprecated('$page->hasVisibleChildren() is deprecated, use $page->hasListedChildren() instead. $page->hasVisibleChildren() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasListedChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a flat child index
|
||||
*
|
||||
* @param bool $drafts
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function index(bool $drafts = false)
|
||||
{
|
||||
if ($drafts === true) {
|
||||
return $this->childrenAndDrafts()->index($drafts);
|
||||
} else {
|
||||
return $this->children()->index();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Children collection
|
||||
*
|
||||
* @param array|null $children
|
||||
* @return self
|
||||
*/
|
||||
protected function setChildren(array $children = null)
|
||||
{
|
||||
if ($children !== null) {
|
||||
$this->children = Pages::factory($children, $this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Drafts collection
|
||||
*
|
||||
* @param array|null $drafts
|
||||
* @return self
|
||||
*/
|
||||
protected function setDrafts(array $drafts = null)
|
||||
{
|
||||
if ($drafts !== null) {
|
||||
$this->drafts = Pages::factory($drafts, $this, true);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
226
kirby/src/Cms/HasFiles.php
Normal file
226
kirby/src/Cms/HasFiles.php
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* HasFiles
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait HasFiles
|
||||
{
|
||||
/**
|
||||
* The Files collection
|
||||
*
|
||||
* @var \Kirby\Cms\Files
|
||||
*/
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type audio
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function audio()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'audio');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type code
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function code()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of file ids
|
||||
* for the toArray method of the model
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function convertFilesToArray(): array
|
||||
{
|
||||
return $this->files()->keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file
|
||||
*
|
||||
* @param array $props
|
||||
* @return \Kirby\Cms\File
|
||||
*/
|
||||
public function createFile(array $props)
|
||||
{
|
||||
$props = array_merge($props, [
|
||||
'parent' => $this,
|
||||
'url' => null
|
||||
]);
|
||||
|
||||
return File::create($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type documents
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function documents()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'document');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific file by filename or the first one
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $in
|
||||
* @return \Kirby\Cms\File|null
|
||||
*/
|
||||
public function file(string $filename = null, string $in = 'files')
|
||||
{
|
||||
if ($filename === null) {
|
||||
return $this->$in()->first();
|
||||
}
|
||||
|
||||
if (strpos($filename, '/') !== false) {
|
||||
$path = dirname($filename);
|
||||
$filename = basename($filename);
|
||||
|
||||
if ($page = $this->find($path)) {
|
||||
return $page->$in()->find($filename);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->$in()->find($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Files collection
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function files()
|
||||
{
|
||||
if (is_a($this->files, 'Kirby\Cms\Files') === true) {
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
return $this->files = Files::factory($this->inventory()['files'], $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Files collection has any audio files
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAudio(): bool
|
||||
{
|
||||
return $this->audio()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Files collection has any code files
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasCode(): bool
|
||||
{
|
||||
return $this->code()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Files collection has any document files
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDocuments(): bool
|
||||
{
|
||||
return $this->documents()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Files collection has any files
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFiles(): bool
|
||||
{
|
||||
return $this->files()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Files collection has any images
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasImages(): bool
|
||||
{
|
||||
return $this->images()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Files collection has any videos
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasVideos(): bool
|
||||
{
|
||||
return $this->videos()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific image by filename or the first one
|
||||
*
|
||||
* @param string $filename
|
||||
* @return \Kirby\Cms\File|null
|
||||
*/
|
||||
public function image(string $filename = null)
|
||||
{
|
||||
return $this->file($filename, 'images');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type image
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function images()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Files collection
|
||||
*
|
||||
* @param \Kirby\Cms\Files|null $files
|
||||
* @return self
|
||||
*/
|
||||
protected function setFiles(array $files = null)
|
||||
{
|
||||
if ($files !== null) {
|
||||
$this->files = Files::factory($files, $this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type videos
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function videos()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'video');
|
||||
}
|
||||
}
|
||||
79
kirby/src/Cms/HasMethods.php
Normal file
79
kirby/src/Cms/HasMethods.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
|
||||
/**
|
||||
* HasMethods
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait HasMethods
|
||||
{
|
||||
/**
|
||||
* All registered methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $methods = [];
|
||||
|
||||
/**
|
||||
* Calls a registered method class with the
|
||||
* passed arguments
|
||||
*
|
||||
* @internal
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function callMethod(string $method, array $args = [])
|
||||
{
|
||||
$closure = $this->getMethod($method);
|
||||
|
||||
if ($closure === null) {
|
||||
throw new BadMethodCallException('The method ' . $method . ' does not exist');
|
||||
}
|
||||
|
||||
return $closure->call($this, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object has a registered method
|
||||
*
|
||||
* @internal
|
||||
* @param string $method
|
||||
* @return bool
|
||||
*/
|
||||
public function hasMethod(string $method): bool
|
||||
{
|
||||
return $this->getMethod($method) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a registered method by name, either from
|
||||
* the current class or from a parent class ordered by
|
||||
* inheritance order (top to bottom)
|
||||
*
|
||||
* @param string $method
|
||||
* @return Closure|null
|
||||
*/
|
||||
protected function getMethod(string $method)
|
||||
{
|
||||
if (isset(static::$methods[$method]) === true) {
|
||||
return static::$methods[$method];
|
||||
}
|
||||
|
||||
foreach (class_parents($this) as $parent) {
|
||||
if (isset($parent::$methods[$method]) === true) {
|
||||
return $parent::$methods[$method];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
182
kirby/src/Cms/HasSiblings.php
Normal file
182
kirby/src/Cms/HasSiblings.php
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* This trait is used by pages, files and users
|
||||
* to handle navigation through parent collections
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait HasSiblings
|
||||
{
|
||||
/**
|
||||
* Returns the position / index in the collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function indexOf($collection = null): int
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
return $collection->indexOf($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next item in the collection if available
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Model|null
|
||||
*/
|
||||
public function next($collection = null)
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
return $collection->nth($this->indexOf($collection) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the end of the collection starting after the current item
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function nextAll($collection = null)
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
return $collection->slice($this->indexOf($collection) + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous item in the collection if available
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Model|null
|
||||
*/
|
||||
public function prev($collection = null)
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
return $collection->nth($this->indexOf($collection) - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the beginning of the collection before the current item
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function prevAll($collection = null)
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
return $collection->slice(0, $this->indexOf($collection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all sibling elements
|
||||
*
|
||||
* @param bool $self
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function siblings(bool $self = true)
|
||||
{
|
||||
$siblings = $this->siblingsCollection();
|
||||
|
||||
if ($self === false) {
|
||||
return $siblings->not($this);
|
||||
}
|
||||
|
||||
return $siblings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a next item in the collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNext($collection = null): bool
|
||||
{
|
||||
return $this->next($collection) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a previous item in the collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrev($collection = null): bool
|
||||
{
|
||||
return $this->prev($collection) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item is the first in the collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFirst($collection = null): bool
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
return $collection->first()->is($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item is the last in the collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLast($collection = null): bool
|
||||
{
|
||||
if ($collection === null) {
|
||||
$collection = $this->siblingsCollection();
|
||||
}
|
||||
|
||||
return $collection->last()->is($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item is at a certain position
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
* @param int $n
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNth(int $n, $collection = null): bool
|
||||
{
|
||||
return $this->indexOf($collection) === $n;
|
||||
}
|
||||
}
|
||||
30
kirby/src/Cms/Html.php
Normal file
30
kirby/src/Cms/Html.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The `Html` class provides methods for building
|
||||
* common HTML tags and also contains some helper
|
||||
* methods.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Html extends \Kirby\Toolkit\Html
|
||||
{
|
||||
/**
|
||||
* Generates an `a` tag with an absolute Url
|
||||
*
|
||||
* @param string $href Relative or absolute Url
|
||||
* @param string|array|null $text If `null`, the link will be used as link text. If an array is passed, each element will be added unencoded
|
||||
* @param array $attr Additional attributes for the a tag.
|
||||
* @return string
|
||||
*/
|
||||
public static function link(string $href = null, $text = null, array $attr = []): string
|
||||
{
|
||||
return parent::link(Url::to($href), $text, $attr);
|
||||
}
|
||||
}
|
||||
95
kirby/src/Cms/Ingredients.php
Normal file
95
kirby/src/Cms/Ingredients.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The Ingredients class is the foundation for
|
||||
* `$kirby->urls()` and `$kirby->roots()` objects.
|
||||
* Those are configured in `kirby/config/urls.php`
|
||||
* and `kirby/config/roots.php`
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Ingredients
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $ingredients = [];
|
||||
|
||||
/**
|
||||
* Creates a new ingredient collection
|
||||
*
|
||||
* @param array $ingredients
|
||||
*/
|
||||
public function __construct(array $ingredients)
|
||||
{
|
||||
$this->ingredients = $ingredients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter for single ingredients
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $args = null)
|
||||
{
|
||||
return $this->ingredients[$method] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->ingredients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single ingredient by key
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $key)
|
||||
{
|
||||
return $this->ingredients[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves all ingredient callbacks
|
||||
* and creates a plain array
|
||||
*
|
||||
* @internal
|
||||
* @param array $ingredients
|
||||
* @return self
|
||||
*/
|
||||
public static function bake(array $ingredients)
|
||||
{
|
||||
foreach ($ingredients as $name => $ingredient) {
|
||||
if (is_a($ingredient, 'Closure') === true) {
|
||||
$ingredients[$name] = $ingredient($ingredients);
|
||||
}
|
||||
}
|
||||
|
||||
return new static($ingredients);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all ingredients as plain array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->ingredients;
|
||||
}
|
||||
}
|
||||
60
kirby/src/Cms/KirbyTag.php
Normal file
60
kirby/src/Cms/KirbyTag.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extended KirbyTag class to provide
|
||||
* common helpers for tag objects
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class KirbyTag extends \Kirby\Text\KirbyTag
|
||||
{
|
||||
/**
|
||||
* Finds a file for the given path.
|
||||
* The method first searches the file
|
||||
* in the current parent, if it's a page.
|
||||
* Afterwards it uses Kirby's global file finder.
|
||||
*
|
||||
* @param string $path
|
||||
* @return \Kirby\Cms\File|null
|
||||
*/
|
||||
public function file(string $path)
|
||||
{
|
||||
$parent = $this->parent();
|
||||
|
||||
if (method_exists($parent, 'file') === true && $file = $parent->file($path)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
if (is_a($parent, 'Kirby\Cms\File') === true && $file = $parent->page()->file($path)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return $this->kirby()->file($path, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Kirby instance
|
||||
*
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
{
|
||||
return $this->data['kirby'] ?? App::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model
|
||||
*
|
||||
* @return \Kirby\Cms\Model|null
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->data['parent'];
|
||||
}
|
||||
}
|
||||
45
kirby/src/Cms/KirbyTags.php
Normal file
45
kirby/src/Cms/KirbyTags.php
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of `Kirby\Text\KirbyTags` that introduces
|
||||
* `kirbytags:before` and `kirbytags:after` hooks
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class KirbyTags extends \Kirby\Text\KirbyTags
|
||||
{
|
||||
/**
|
||||
* The KirbyTag rendering class
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $tagClass = 'Kirby\Cms\KirbyTag';
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param array $data
|
||||
* @param array $options
|
||||
* @param \Kirby\Cms\App $app
|
||||
* @return string
|
||||
*/
|
||||
public static function parse(string $text = null, array $data = [], array $options = [], ?App $app = null): string
|
||||
{
|
||||
if ($app !== null) {
|
||||
$text = $app->apply('kirbytags:before', compact('text', 'data', 'options'), 'text');
|
||||
}
|
||||
|
||||
$text = parent::parse($text, $data, $options);
|
||||
|
||||
if ($app !== null) {
|
||||
$text = $app->apply('kirbytags:after', compact('text', 'data', 'options'), 'text');
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
726
kirby/src/Cms/Language.php
Normal file
726
kirby/src/Cms/Language.php
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The `$language` object represents
|
||||
* a single language in a multi-language
|
||||
* Kirby setup. You can, for example,
|
||||
* use the methods of this class to get
|
||||
* the name or locale of a language,
|
||||
* check for the default language,
|
||||
* get translation strings and many
|
||||
* more things.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Language extends Model
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $default;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $direction;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $locale;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected $slugs;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected $smartypants;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected $translations;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Creates a new language object
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->setRequiredProperties($props, [
|
||||
'code'
|
||||
]);
|
||||
|
||||
$this->setOptionalProperties($props, [
|
||||
'default',
|
||||
'direction',
|
||||
'locale',
|
||||
'name',
|
||||
'slugs',
|
||||
'smartypants',
|
||||
'translations',
|
||||
'url',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language code
|
||||
* when the language is converted to a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base Url for the language
|
||||
* without the path or other cruft
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function baseUrl(): string
|
||||
{
|
||||
$kirbyUrl = $this->kirby()->url();
|
||||
$languageUrl = $this->url();
|
||||
|
||||
if (empty($this->url)) {
|
||||
return $kirbyUrl;
|
||||
}
|
||||
|
||||
if (Str::startsWith($languageUrl, $kirbyUrl) === true) {
|
||||
return $kirbyUrl;
|
||||
}
|
||||
|
||||
return Url::base($languageUrl) ?? $kirbyUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the language code/id.
|
||||
* The language code is used in
|
||||
* text file names as appendix.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function code(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal converter to create or remove
|
||||
* translation files.
|
||||
*
|
||||
* @param string $from
|
||||
* @param string $to
|
||||
* @return bool
|
||||
*/
|
||||
protected static function converter(string $from, string $to): bool
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$site = $kirby->site();
|
||||
|
||||
// convert site
|
||||
foreach ($site->files() as $file) {
|
||||
F::move($file->contentFile($from, true), $file->contentFile($to, true));
|
||||
}
|
||||
|
||||
F::move($site->contentFile($from, true), $site->contentFile($to, true));
|
||||
|
||||
// convert all pages
|
||||
foreach ($kirby->site()->index(true) as $page) {
|
||||
foreach ($page->files() as $file) {
|
||||
F::move($file->contentFile($from, true), $file->contentFile($to, true));
|
||||
}
|
||||
|
||||
F::move($page->contentFile($from, true), $page->contentFile($to, true));
|
||||
}
|
||||
|
||||
// convert all users
|
||||
foreach ($kirby->users() as $user) {
|
||||
foreach ($user->files() as $file) {
|
||||
F::move($file->contentFile($from, true), $file->contentFile($to, true));
|
||||
}
|
||||
|
||||
F::move($user->contentFile($from, true), $user->contentFile($to, true));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new language object
|
||||
*
|
||||
* @internal
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public static function create(array $props)
|
||||
{
|
||||
$props['code'] = Str::slug($props['code'] ?? null);
|
||||
$kirby = App::instance();
|
||||
$languages = $kirby->languages();
|
||||
|
||||
// make the first language the default language
|
||||
if ($languages->count() === 0) {
|
||||
$props['default'] = true;
|
||||
}
|
||||
|
||||
$language = new static($props);
|
||||
|
||||
// validate the new language
|
||||
LanguageRules::create($language);
|
||||
|
||||
$language->save();
|
||||
|
||||
if ($languages->count() === 0) {
|
||||
static::converter('', $language->code());
|
||||
}
|
||||
|
||||
return $language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current language and
|
||||
* all its translation files
|
||||
*
|
||||
* @internal
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
if ($this->exists() === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$kirby = App::instance();
|
||||
$languages = $kirby->languages();
|
||||
$code = $this->code();
|
||||
|
||||
if (F::remove($this->root()) !== true) {
|
||||
throw new Exception('The language could not be deleted');
|
||||
}
|
||||
|
||||
if ($languages->count() === 1) {
|
||||
return $this->converter($code, '');
|
||||
} else {
|
||||
return $this->deleteContentFiles($code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the language is deleted, all content files with
|
||||
* the language code must be removed as well.
|
||||
*
|
||||
* @param mixed $code
|
||||
* @return bool
|
||||
*/
|
||||
protected function deleteContentFiles($code): bool
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$site = $kirby->site();
|
||||
|
||||
F::remove($site->contentFile($code, true));
|
||||
|
||||
foreach ($kirby->site()->index(true) as $page) {
|
||||
foreach ($page->files() as $file) {
|
||||
F::remove($file->contentFile($code, true));
|
||||
}
|
||||
|
||||
F::remove($page->contentFile($code, true));
|
||||
}
|
||||
|
||||
foreach ($kirby->users() as $user) {
|
||||
foreach ($user->files() as $file) {
|
||||
F::remove($file->contentFile($code, true));
|
||||
}
|
||||
|
||||
F::remove($user->contentFile($code, true));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reading direction of this language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function direction(): string
|
||||
{
|
||||
return $this->direction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the language file exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is the default language
|
||||
* for the site.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isDefault(): bool
|
||||
{
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
/**
|
||||
* The id is required for collections
|
||||
* to work properly. The code is used as id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PHP locale setting array
|
||||
*
|
||||
* @param int $category If passed, returns the locale for the specified category (e.g. LC_ALL) as string
|
||||
* @return array|string
|
||||
*/
|
||||
public function locale(int $category = null)
|
||||
{
|
||||
if ($category !== null) {
|
||||
return $this->locale[$category] ?? $this->locale[LC_ALL] ?? null;
|
||||
} else {
|
||||
return $this->locale;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale array but with the locale
|
||||
* constants replaced with their string representations
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function localeExport(): array
|
||||
{
|
||||
// list of all possible constant names
|
||||
$constantNames = [
|
||||
'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY',
|
||||
'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'
|
||||
];
|
||||
|
||||
// build an associative array with the locales
|
||||
// that are actually supported on this system
|
||||
$constants = [];
|
||||
foreach ($constantNames as $name) {
|
||||
if (defined($name) === true) {
|
||||
$constants[constant($name)] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// replace the keys in the locale data array with the locale names
|
||||
$return = [];
|
||||
foreach ($this->locale() as $key => $value) {
|
||||
if (isset($constants[$key]) === true) {
|
||||
// the key is a valid constant,
|
||||
// replace it with its string representation
|
||||
$return[$constants[$key]] = $value;
|
||||
} else {
|
||||
// not found, keep it as-is
|
||||
$return[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable name
|
||||
* of the language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL path for the language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function path(): string
|
||||
{
|
||||
if ($this->url === null) {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
return Url::path($this->url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the routing pattern for the language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function pattern(): string
|
||||
{
|
||||
$path = $this->path();
|
||||
|
||||
if (empty($path) === true) {
|
||||
return '(:all)';
|
||||
}
|
||||
|
||||
return $path . '/(:all?)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the language file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
return App::instance()->root('languages') . '/' . $this->code() . '.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the LanguageRouter instance
|
||||
* which is used to handle language specific
|
||||
* routes.
|
||||
*
|
||||
* @return \Kirby\Cms\LanguageRouter
|
||||
*/
|
||||
public function router()
|
||||
{
|
||||
return new LanguageRouter($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slug rules for language
|
||||
*
|
||||
* @internal
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$code = $this->locale(LC_CTYPE);
|
||||
$code = Str::contains($code, '.') ? Str::before($code, '.') : $code;
|
||||
$file = $this->kirby()->root('i18n:rules') . '/' . $code . '.json';
|
||||
|
||||
if (F::exists($file) === false) {
|
||||
$file = $this->kirby()->root('i18n:rules') . '/' . Str::before($code, '_') . '.json';
|
||||
}
|
||||
|
||||
try {
|
||||
$data = Data::read($file);
|
||||
} catch (\Exception $e) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
return array_merge($data, $this->slugs());
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the language settings in the languages folder
|
||||
*
|
||||
* @internal
|
||||
* @return self
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
try {
|
||||
$existingData = Data::read($this->root());
|
||||
} catch (Throwable $e) {
|
||||
$existingData = [];
|
||||
}
|
||||
|
||||
$props = [
|
||||
'code' => $this->code(),
|
||||
'default' => $this->isDefault(),
|
||||
'direction' => $this->direction(),
|
||||
'locale' => $this->localeExport(),
|
||||
'name' => $this->name(),
|
||||
'translations' => $this->translations(),
|
||||
'url' => $this->url,
|
||||
];
|
||||
|
||||
$data = array_merge($existingData, $props);
|
||||
|
||||
ksort($data);
|
||||
|
||||
Data::write($this->root(), $data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @return self
|
||||
*/
|
||||
protected function setCode(string $code)
|
||||
{
|
||||
$this->code = trim($code);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $default
|
||||
* @return self
|
||||
*/
|
||||
protected function setDefault(bool $default = false)
|
||||
{
|
||||
$this->default = $default;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $direction
|
||||
* @return self
|
||||
*/
|
||||
protected function setDirection(string $direction = 'ltr')
|
||||
{
|
||||
$this->direction = $direction === 'rtl' ? 'rtl' : 'ltr';
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $locale
|
||||
* @return self
|
||||
*/
|
||||
protected function setLocale($locale = null)
|
||||
{
|
||||
if (is_array($locale)) {
|
||||
// replace string constant keys with the constant values
|
||||
$convertedLocale = [];
|
||||
foreach ($locale as $key => $value) {
|
||||
if (is_string($key) === true && Str::startsWith($key, 'LC_') === true) {
|
||||
$key = constant($key);
|
||||
}
|
||||
|
||||
$convertedLocale[$key] = $value;
|
||||
}
|
||||
|
||||
$this->locale = $convertedLocale;
|
||||
} elseif (is_string($locale)) {
|
||||
$this->locale = [LC_ALL => $locale];
|
||||
} elseif ($locale === null) {
|
||||
$this->locale = [LC_ALL => $this->code];
|
||||
} else {
|
||||
throw new InvalidArgumentException('Locale must be string or array');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
protected function setName(string $name = null)
|
||||
{
|
||||
$this->name = trim($name ?? $this->code);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $slugs
|
||||
* @return self
|
||||
*/
|
||||
protected function setSlugs(array $slugs = null)
|
||||
{
|
||||
$this->slugs = $slugs ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $smartypants
|
||||
* @return self
|
||||
*/
|
||||
protected function setSmartypants(array $smartypants = null)
|
||||
{
|
||||
$this->smartypants = $smartypants ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $translations
|
||||
* @return self
|
||||
*/
|
||||
protected function setTranslations(array $translations = null)
|
||||
{
|
||||
$this->translations = $translations ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return self
|
||||
*/
|
||||
protected function setUrl(string $url = null)
|
||||
{
|
||||
$this->url = $url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom slug rules for this language
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function slugs(): array
|
||||
{
|
||||
return $this->slugs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom SmartyPants options for this language
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function smartypants(): array
|
||||
{
|
||||
return $this->smartypants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the most important
|
||||
* properties as array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->code(),
|
||||
'default' => $this->isDefault(),
|
||||
'direction' => $this->direction(),
|
||||
'locale' => $this->locale(),
|
||||
'name' => $this->name(),
|
||||
'rules' => $this->rules(),
|
||||
'url' => $this->url()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation strings for this language
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function translations(): array
|
||||
{
|
||||
return $this->translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute Url for the language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function url(): string
|
||||
{
|
||||
$url = $this->url;
|
||||
|
||||
if ($url === null) {
|
||||
$url = '/' . $this->code;
|
||||
}
|
||||
|
||||
return Url::makeAbsolute($url, $this->kirby()->url());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update language properties and save them
|
||||
*
|
||||
* @internal
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public function update(array $props = null)
|
||||
{
|
||||
// don't change the language code
|
||||
unset($props['code']);
|
||||
|
||||
// make sure the slug is nice and clean
|
||||
$props['slug'] = Str::slug($props['slug'] ?? null);
|
||||
|
||||
$kirby = App::instance();
|
||||
$updated = $this->clone($props);
|
||||
|
||||
// validate the updated language
|
||||
LanguageRules::update($updated);
|
||||
|
||||
// convert the current default to a non-default language
|
||||
if ($updated->isDefault() === true) {
|
||||
if ($oldDefault = $kirby->defaultLanguage()) {
|
||||
$oldDefault->clone(['default' => false])->save();
|
||||
}
|
||||
|
||||
$code = $this->code();
|
||||
$site = $kirby->site();
|
||||
|
||||
touch($site->contentFile($code));
|
||||
|
||||
foreach ($kirby->site()->index(true) as $page) {
|
||||
$files = $page->files();
|
||||
|
||||
foreach ($files as $file) {
|
||||
touch($file->contentFile($code));
|
||||
}
|
||||
|
||||
touch($page->contentFile($code));
|
||||
}
|
||||
} elseif ($this->isDefault() === true) {
|
||||
throw new PermissionException('Please select another language to be the primary language');
|
||||
}
|
||||
|
||||
return $updated->save();
|
||||
}
|
||||
}
|
||||
134
kirby/src/Cms/LanguageRouter.php
Normal file
134
kirby/src/Cms/LanguageRouter.php
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Http\Router;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The language router is used internally
|
||||
* to handle language-specific (scoped) routes
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class LanguageRouter
|
||||
{
|
||||
/**
|
||||
* The parent language
|
||||
*
|
||||
* @var Language
|
||||
*/
|
||||
protected $language;
|
||||
|
||||
/**
|
||||
* The router instance
|
||||
*
|
||||
* @var Router
|
||||
*/
|
||||
protected $router;
|
||||
|
||||
/**
|
||||
* Creates a new language router instance
|
||||
* for the given language
|
||||
*
|
||||
* @param \Kirby\Cms\Language $language
|
||||
*/
|
||||
public function __construct(Language $language)
|
||||
{
|
||||
$this->language = $language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all scoped routes for the
|
||||
* current language from the Kirby instance
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function routes(): array
|
||||
{
|
||||
$language = $this->language;
|
||||
$kirby = $language->kirby();
|
||||
$routes = $kirby->routes();
|
||||
|
||||
// only keep the scoped language routes
|
||||
$routes = array_values(array_filter($routes, function ($route) use ($language) {
|
||||
|
||||
// no language scope
|
||||
if (empty($route['language']) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// wildcard
|
||||
if ($route['language'] === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// get all applicable languages
|
||||
$languages = Str::split(strtolower($route['language']), '|');
|
||||
|
||||
// validate the language
|
||||
return in_array($language->code(), $languages) === true;
|
||||
}));
|
||||
|
||||
// add the page-scope if necessary
|
||||
foreach ($routes as $index => $route) {
|
||||
if ($pageId = ($route['page'] ?? null)) {
|
||||
if ($page = $kirby->page($pageId)) {
|
||||
|
||||
// convert string patterns to arrays
|
||||
$patterns = A::wrap($route['pattern']);
|
||||
|
||||
// prefix all patterns with the page slug
|
||||
$patterns = array_map(function ($pattern) use ($page, $language) {
|
||||
return $page->uri($language) . '/' . $pattern;
|
||||
}, $patterns);
|
||||
|
||||
// reinject the pattern and the full page object
|
||||
$routes[$index]['pattern'] = $patterns;
|
||||
$routes[$index]['page'] = $page;
|
||||
} else {
|
||||
throw new NotFoundException('The page "' . $pageId . '" does not exist');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the Router::call method
|
||||
* that injects the Language instance and
|
||||
* if needed also the Page as arguments.
|
||||
*
|
||||
* @param string|null $path
|
||||
* @return mixed
|
||||
*/
|
||||
public function call(string $path = null)
|
||||
{
|
||||
$language = $this->language;
|
||||
$kirby = $language->kirby();
|
||||
$router = new Router($this->routes());
|
||||
|
||||
try {
|
||||
return $router->call($path, $kirby->request()->method(), function ($route) use ($kirby, $language) {
|
||||
$kirby->setCurrentTranslation($language);
|
||||
$kirby->setCurrentLanguage($language);
|
||||
|
||||
if ($page = $route->page()) {
|
||||
return $route->action()->call($route, $language, $page, ...$route->arguments());
|
||||
} else {
|
||||
return $route->action()->call($route, $language, ...$route->arguments());
|
||||
}
|
||||
});
|
||||
} catch (Exception $e) {
|
||||
return $kirby->resolve($path, $language->code());
|
||||
}
|
||||
}
|
||||
}
|
||||
154
kirby/src/Cms/LanguageRoutes.php
Normal file
154
kirby/src/Cms/LanguageRoutes.php
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
class LanguageRoutes
|
||||
{
|
||||
/**
|
||||
* Creates all multi-language routes
|
||||
*
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
* @return array
|
||||
*/
|
||||
public static function create(App $kirby): array
|
||||
{
|
||||
$routes = [];
|
||||
|
||||
// add the route for the home page
|
||||
$routes[] = static::home($kirby);
|
||||
|
||||
// Kirby's base url
|
||||
$baseurl = $kirby->url();
|
||||
|
||||
foreach ($kirby->languages() as $language) {
|
||||
|
||||
// ignore languages with a different base url
|
||||
if ($language->baseurl() !== $baseurl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$routes[] = [
|
||||
'pattern' => $language->pattern(),
|
||||
'method' => 'ALL',
|
||||
'env' => 'site',
|
||||
'action' => function ($path = null) use ($language) {
|
||||
if ($result = $language->router()->call($path)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// jump through to the fallback if nothing
|
||||
// can be found for this language
|
||||
$this->next();
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
$routes[] = static::fallback($kirby);
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create the fallback route
|
||||
* for unprefixed default language URLs.
|
||||
*
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
* @return array
|
||||
*/
|
||||
public static function fallback(App $kirby): array
|
||||
{
|
||||
return [
|
||||
'pattern' => '(:all)',
|
||||
'method' => 'ALL',
|
||||
'env' => 'site',
|
||||
'action' => function (string $path) use ($kirby) {
|
||||
|
||||
// check for content representations or files
|
||||
$extension = F::extension($path);
|
||||
|
||||
// try to redirect prefixed pages
|
||||
if (empty($extension) === true && $page = $kirby->page($path)) {
|
||||
$url = $kirby->request()->url([
|
||||
'query' => null,
|
||||
'params' => null,
|
||||
'fragment' => null
|
||||
]);
|
||||
|
||||
if ($url->toString() !== $page->url()) {
|
||||
// redirect to translated page directly
|
||||
// if translation is exists and languages detect is enabled
|
||||
if (
|
||||
$kirby->option('languages.detect') === true &&
|
||||
$page->translation($kirby->detectedLanguage()->code())->exists() === true
|
||||
) {
|
||||
return $kirby
|
||||
->response()
|
||||
->redirect($page->url($kirby->detectedLanguage()->code()));
|
||||
}
|
||||
|
||||
return $kirby
|
||||
->response()
|
||||
->redirect($page->url());
|
||||
}
|
||||
}
|
||||
|
||||
return $kirby->defaultLanguage()->router()->call($path);
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the multi-language home page route
|
||||
*
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
* @return array
|
||||
*/
|
||||
public static function home(App $kirby): array
|
||||
{
|
||||
// Multi-language home
|
||||
return [
|
||||
'pattern' => '',
|
||||
'method' => 'ALL',
|
||||
'env' => 'site',
|
||||
'action' => function () use ($kirby) {
|
||||
|
||||
// find all languages with the same base url as the current installation
|
||||
$languages = $kirby->languages()->filterBy('baseurl', $kirby->url());
|
||||
|
||||
// if there's no language with a matching base url,
|
||||
// redirect to the default language
|
||||
if ($languages->count() === 0) {
|
||||
return $kirby
|
||||
->response()
|
||||
->redirect($kirby->defaultLanguage()->url());
|
||||
}
|
||||
|
||||
// if there's just one language, we take that to render the home page
|
||||
if ($languages->count() === 1) {
|
||||
$currentLanguage = $languages->first();
|
||||
} else {
|
||||
$currentLanguage = $kirby->defaultLanguage();
|
||||
}
|
||||
|
||||
// language detection on the home page with / as URL
|
||||
if ($kirby->url() !== $currentLanguage->url()) {
|
||||
if ($kirby->option('languages.detect') === true) {
|
||||
return $kirby
|
||||
->response()
|
||||
->redirect($kirby->detectedLanguage()->url());
|
||||
}
|
||||
|
||||
return $kirby
|
||||
->response()
|
||||
->redirect($currentLanguage->url());
|
||||
}
|
||||
|
||||
// render the home page of the current language
|
||||
return $currentLanguage->router()->call();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
72
kirby/src/Cms/LanguageRules.php
Normal file
72
kirby/src/Cms/LanguageRules.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Validators for all language actions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class LanguageRules
|
||||
{
|
||||
public static function create(Language $language): bool
|
||||
{
|
||||
static::validLanguageCode($language);
|
||||
static::validLanguageName($language);
|
||||
|
||||
if ($language->exists() === true) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'language.duplicate',
|
||||
'data' => [
|
||||
'code' => $language->code()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function update(Language $language)
|
||||
{
|
||||
static::validLanguageCode($language);
|
||||
static::validLanguageName($language);
|
||||
}
|
||||
|
||||
public static function validLanguageCode(Language $language): bool
|
||||
{
|
||||
if (Str::length($language->code()) < 2) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'language.code',
|
||||
'data' => [
|
||||
'code' => $language->code(),
|
||||
'name' => $language->name()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validLanguageName(Language $language): bool
|
||||
{
|
||||
if (Str::length($language->name()) < 1) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'language.name',
|
||||
'data' => [
|
||||
'code' => $language->code(),
|
||||
'name' => $language->name()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
109
kirby/src/Cms/Languages.php
Normal file
109
kirby/src/Cms/Languages.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* A collection of all defined site languages
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Languages extends Collection
|
||||
{
|
||||
/**
|
||||
* Creates a new collection with the given language objects
|
||||
*
|
||||
* @param array $objects
|
||||
* @param object $parent
|
||||
*/
|
||||
public function __construct($objects = [], $parent = null)
|
||||
{
|
||||
$defaults = array_filter($objects, function ($language) {
|
||||
return $language->isDefault() === true;
|
||||
});
|
||||
|
||||
if (count($defaults) > 1) {
|
||||
throw new DuplicateException('You cannot have multiple default languages. Please check your language config files.');
|
||||
}
|
||||
|
||||
parent::__construct($objects, $parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all language codes as array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function codes(): array
|
||||
{
|
||||
return $this->keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new language with the given props
|
||||
*
|
||||
* @internal
|
||||
* @param array $props
|
||||
* @return \Kirby\Cms\Language
|
||||
*/
|
||||
public function create(array $props)
|
||||
{
|
||||
return Language::create($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default language
|
||||
*
|
||||
* @return \Kirby\Cms\Language|null
|
||||
*/
|
||||
public function default()
|
||||
{
|
||||
if ($language = $this->findBy('isDefault', true)) {
|
||||
return $language;
|
||||
} else {
|
||||
return $this->first();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Languages::default()` instead
|
||||
* @return \Kirby\Cms\Language|null
|
||||
*/
|
||||
public function findDefault()
|
||||
{
|
||||
deprecated('$languages->findDefault() is deprecated, use $languages->default() instead. $languages->findDefault() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->default();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all defined languages to a collection
|
||||
*
|
||||
* @internal
|
||||
* @return self
|
||||
*/
|
||||
public static function load()
|
||||
{
|
||||
$languages = [];
|
||||
$files = glob(App::instance()->root('languages') . '/*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$props = F::load($file);
|
||||
|
||||
if (is_array($props) === true) {
|
||||
// inject the language code from the filename if it does not exist
|
||||
$props['code'] = $props['code'] ?? F::name($file);
|
||||
|
||||
$languages[] = new Language($props);
|
||||
}
|
||||
}
|
||||
|
||||
return new static($languages);
|
||||
}
|
||||
}
|
||||
166
kirby/src/Cms/Media.php
Normal file
166
kirby/src/Cms/Media.php
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Handles all tasks to get the Media API
|
||||
* up and running and link files correctly
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Media
|
||||
{
|
||||
/**
|
||||
* Tries to find a file by model and filename
|
||||
* and to copy it to the media folder.
|
||||
*
|
||||
* @param \Kirby\Cms\Model $model
|
||||
* @param string $hash
|
||||
* @param string $filename
|
||||
* @return \Kirby\Cms\Response|false
|
||||
*/
|
||||
public static function link(Model $model = null, string $hash, string $filename)
|
||||
{
|
||||
if ($model === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// fix issues with spaces in filenames
|
||||
$filename = urldecode($filename);
|
||||
|
||||
// try to find a file by model and filename
|
||||
// this should work for all original files
|
||||
if ($file = $model->file($filename)) {
|
||||
|
||||
// check if the request contained an outdated media hash
|
||||
if ($file->mediaHash() !== $hash) {
|
||||
// if at least the token was correct, redirect
|
||||
if (Str::startsWith($hash, $file->mediaToken() . '-') === true) {
|
||||
return Response::redirect($file->mediaUrl(), 307);
|
||||
} else {
|
||||
// don't leak the correct token
|
||||
return new Response('Not Found', 'text/plain', 404);
|
||||
}
|
||||
}
|
||||
|
||||
// send the file to the browser
|
||||
return Response::file($file->publish()->mediaRoot());
|
||||
}
|
||||
|
||||
// try to generate a thumb for the file
|
||||
return static::thumb($model, $hash, $filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the file to the final media folder location
|
||||
*
|
||||
* @param \Kirby\Cms\File $file
|
||||
* @param string $dest
|
||||
* @return bool
|
||||
*/
|
||||
public static function publish(File $file, string $dest): bool
|
||||
{
|
||||
$src = $file->root();
|
||||
$version = dirname($dest);
|
||||
$directory = dirname($version);
|
||||
|
||||
// unpublish all files except stuff in the version folder
|
||||
Media::unpublish($directory, $file, $version);
|
||||
|
||||
// copy/overwrite the file to the dest folder
|
||||
return F::copy($src, $dest, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a job file for the
|
||||
* given filename and then calls the thumb
|
||||
* component to create a thumbnail accordingly
|
||||
*
|
||||
* @param \Kirby\Cms\Model $model
|
||||
* @param string $hash
|
||||
* @param string $filename
|
||||
* @return \Kirby\Cms\Response|false
|
||||
*/
|
||||
public static function thumb($model, string $hash, string $filename)
|
||||
{
|
||||
$kirby = App::instance();
|
||||
|
||||
if (is_string($model) === true) {
|
||||
// assets
|
||||
$root = $kirby->root('media') . '/assets/' . $model . '/' . $hash;
|
||||
} else {
|
||||
// model files
|
||||
$root = $model->mediaRoot() . '/' . $hash;
|
||||
}
|
||||
|
||||
try {
|
||||
$thumb = $root . '/' . $filename;
|
||||
$job = $root . '/.jobs/' . $filename . '.json';
|
||||
$options = Data::read($job);
|
||||
|
||||
if (empty($options) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_string($model) === true) {
|
||||
$source = $kirby->root('index') . '/' . $model . '/' . $options['filename'];
|
||||
} else {
|
||||
$source = $model->file($options['filename'])->root();
|
||||
}
|
||||
|
||||
try {
|
||||
$kirby->thumb($source, $thumb, $options);
|
||||
F::remove($job);
|
||||
return Response::file($thumb);
|
||||
} catch (Throwable $e) {
|
||||
F::remove($thumb);
|
||||
return Response::file($source);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all versions of the given file
|
||||
* within the parent directory
|
||||
*
|
||||
* @param string $directory
|
||||
* @param \Kirby\Cms\File $file
|
||||
* @param string $ignore
|
||||
* @return bool
|
||||
*/
|
||||
public static function unpublish(string $directory, File $file, string $ignore = null): bool
|
||||
{
|
||||
if (is_dir($directory) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// get both old and new versions (pre and post Kirby 3.4.0)
|
||||
$versions = array_merge(
|
||||
glob($directory . '/' . crc32($file->filename()) . '-*', GLOB_ONLYDIR),
|
||||
glob($directory . '/' . $file->mediaToken() . '-*', GLOB_ONLYDIR)
|
||||
);
|
||||
|
||||
// delete all versions of the file
|
||||
foreach ($versions as $version) {
|
||||
if ($version === $ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Dir::remove($version);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
109
kirby/src/Cms/Model.php
Normal file
109
kirby/src/Cms/Model.php
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
||||
/**
|
||||
* Foundation for Page, Site, File and User models.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
abstract class Model
|
||||
{
|
||||
use Properties;
|
||||
|
||||
/**
|
||||
* The parent Kirby instance
|
||||
*
|
||||
* @var \Kirby\Cms\App
|
||||
*/
|
||||
public static $kirby;
|
||||
|
||||
/**
|
||||
* The parent site instance
|
||||
*
|
||||
* @var \Kirby\Cms\Site
|
||||
*/
|
||||
protected $site;
|
||||
|
||||
/**
|
||||
* Makes it possible to convert the entire model
|
||||
* to a string. Mostly useful for debugging
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Each model must return a unique id
|
||||
*
|
||||
* @return string|int
|
||||
*/
|
||||
public function id()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Kirby instance
|
||||
*
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
{
|
||||
return static::$kirby = static::$kirby ?? App::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Site instance
|
||||
*
|
||||
* @return \Kirby\Cms\Site
|
||||
*/
|
||||
public function site()
|
||||
{
|
||||
return $this->site = $this->site ?? $this->kirby()->site();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the parent Kirby object
|
||||
*
|
||||
* @param \Kirby\Cms\App|null $kirby
|
||||
* @return self
|
||||
*/
|
||||
protected function setKirby(App $kirby = null)
|
||||
{
|
||||
static::$kirby = $kirby;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the parent site object
|
||||
*
|
||||
* @internal
|
||||
* @param \Kirby\Cms\Site|null $site
|
||||
* @return self
|
||||
*/
|
||||
public function setSite(Site $site = null)
|
||||
{
|
||||
$this->site = $site;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the model to a simple array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->propertiesToArray();
|
||||
}
|
||||
}
|
||||
95
kirby/src/Cms/ModelPermissions.php
Normal file
95
kirby/src/Cms/ModelPermissions.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
/**
|
||||
* ModelPermissions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
abstract class ModelPermissions
|
||||
{
|
||||
protected $category;
|
||||
protected $model;
|
||||
protected $options;
|
||||
protected $permissions;
|
||||
protected $user;
|
||||
|
||||
public function __call(string $method, array $arguments = []): bool
|
||||
{
|
||||
return $this->can($method);
|
||||
}
|
||||
|
||||
public function __construct(Model $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
$this->options = $model->blueprint()->options();
|
||||
$this->user = $model->kirby()->user() ?? User::nobody();
|
||||
$this->permissions = $this->user->role()->permissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
public function can(string $action): bool
|
||||
{
|
||||
$role = $this->user->role()->id();
|
||||
|
||||
if ($role === 'nobody') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for a custom overall can method
|
||||
if (method_exists($this, 'can' . $action) === true && $this->{'can' . $action}() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// evaluate the blueprint options block
|
||||
if (isset($this->options[$action]) === true) {
|
||||
$options = $this->options[$action];
|
||||
|
||||
if ($options === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($options === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_array($options) === true && A::isAssociative($options) === true) {
|
||||
return $options[$role] ?? $options['*'] ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->permissions->for($this->category, $action);
|
||||
}
|
||||
|
||||
public function cannot(string $action): bool
|
||||
{
|
||||
return $this->can($action) === false;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = [];
|
||||
|
||||
foreach ($this->options as $key => $value) {
|
||||
$array[$key] = $this->can($key);
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
733
kirby/src/Cms/ModelWithContent.php
Normal file
733
kirby/src/Cms/ModelWithContent.php
Normal file
|
|
@ -0,0 +1,733 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* ModelWithContent
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
abstract class ModelWithContent extends Model
|
||||
{
|
||||
/**
|
||||
* Each model must define a CLASS_ALIAS
|
||||
* which will be used in template queries.
|
||||
* The CLASS_ALIAS is a short human-readable
|
||||
* version of the class name. I.e. page.
|
||||
*/
|
||||
const CLASS_ALIAS = null;
|
||||
|
||||
/**
|
||||
* The content
|
||||
*
|
||||
* @var \Kirby\Cms\Content
|
||||
*/
|
||||
public $content;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Translations
|
||||
*/
|
||||
public $translations;
|
||||
|
||||
/**
|
||||
* Returns the blueprint of the model
|
||||
*
|
||||
* @return \Kirby\Cms\Blueprint
|
||||
*/
|
||||
abstract public function blueprint();
|
||||
|
||||
/**
|
||||
* Executes any given model action
|
||||
*
|
||||
* @param string $action
|
||||
* @param array $arguments
|
||||
* @param Closure $callback
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function commit(string $action, array $arguments, Closure $callback);
|
||||
|
||||
/**
|
||||
* Returns the content
|
||||
*
|
||||
* @param string $languageCode
|
||||
* @return \Kirby\Cms\Content
|
||||
*/
|
||||
public function content(string $languageCode = null)
|
||||
{
|
||||
|
||||
// single language support
|
||||
if ($this->kirby()->multilang() === false) {
|
||||
if (is_a($this->content, 'Kirby\Cms\Content') === true) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
return $this->setContent($this->readContent())->content;
|
||||
|
||||
// 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)) {
|
||||
$content = new Content($translation->content(), $this);
|
||||
} else {
|
||||
throw new InvalidArgumentException('Invalid language: ' . $languageCode);
|
||||
}
|
||||
|
||||
// only store the content for the current language
|
||||
if ($languageCode === null) {
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content file
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $languageCode
|
||||
* @param bool $force
|
||||
* @return string
|
||||
*/
|
||||
public function contentFile(string $languageCode = null, bool $force = false): string
|
||||
{
|
||||
$extension = $this->contentFileExtension();
|
||||
$directory = $this->contentFileDirectory();
|
||||
$filename = $this->contentFileName();
|
||||
|
||||
// overwrite the language code
|
||||
if ($force === true) {
|
||||
if (empty($languageCode) === false) {
|
||||
return $directory . '/' . $filename . '.' . $languageCode . '.' . $extension;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all content files
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function contentFiles(): array
|
||||
{
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
$files = [];
|
||||
foreach ($this->kirby()->languages()->codes() as $code) {
|
||||
$files[] = $this->contentFile($code);
|
||||
}
|
||||
return $files;
|
||||
} else {
|
||||
return [
|
||||
$this->contentFile()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the content that should be written
|
||||
* to the text file
|
||||
*
|
||||
* @internal
|
||||
* @param array $data
|
||||
* @param string $languageCode
|
||||
* @return array
|
||||
*/
|
||||
public function contentFileData(array $data, string $languageCode = null): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the
|
||||
* folder in which the content file is
|
||||
* located
|
||||
*
|
||||
* @internal
|
||||
* @return string|null
|
||||
*/
|
||||
public function contentFileDirectory(): ?string
|
||||
{
|
||||
return $this->root();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension of the content file
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileExtension(): string
|
||||
{
|
||||
return $this->kirby()->contentExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be declared by the final model
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
abstract public function contentFileName(): string;
|
||||
|
||||
/**
|
||||
* Decrement a given field value
|
||||
*
|
||||
* @param string $field
|
||||
* @param int $by
|
||||
* @param int $min
|
||||
* @return self
|
||||
*/
|
||||
public function decrement(string $field, int $by = 1, int $min = 0)
|
||||
{
|
||||
$value = (int)$this->content()->get($field)->value() - $by;
|
||||
|
||||
if ($value < $min) {
|
||||
$value = $min;
|
||||
}
|
||||
|
||||
return $this->update([$field => $value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all content validation errors
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function errors(): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($this->blueprint()->sections() as $section) {
|
||||
if (method_exists($section, 'errors') === true || isset($section->errors)) {
|
||||
$errors = array_merge($errors, $section->errors());
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment a given field value
|
||||
*
|
||||
* @param string $field
|
||||
* @param int $by
|
||||
* @param int $max
|
||||
* @return self
|
||||
*/
|
||||
public function increment(string $field, int $by = 1, int $max = null)
|
||||
{
|
||||
$value = (int)$this->content()->get($field)->value() + $by;
|
||||
|
||||
if ($max && $value > $max) {
|
||||
$value = $max;
|
||||
}
|
||||
|
||||
return $this->update([$field => $value]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model is locked for the current user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocked(): bool
|
||||
{
|
||||
$lock = $this->lock();
|
||||
return $lock && $lock->isLocked() === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the data has any errors
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return Form::for($this)->hasErrors() === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lock object for this model
|
||||
*
|
||||
* Only if a content directory exists,
|
||||
* virtual pages will need to overwrite this method
|
||||
*
|
||||
* @return \Kirby\Cms\ContentLock|null
|
||||
*/
|
||||
public function lock()
|
||||
{
|
||||
$dir = $this->contentFileDirectory();
|
||||
|
||||
if (
|
||||
$this->kirby()->option('content.locking', true) &&
|
||||
is_string($dir) === true &&
|
||||
file_exists($dir) === true
|
||||
) {
|
||||
return new ContentLock($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the panel icon definition
|
||||
*
|
||||
* @internal
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function panelIcon(array $params = null): array
|
||||
{
|
||||
$defaults = [
|
||||
'type' => 'page',
|
||||
'ratio' => null,
|
||||
'back' => 'pattern',
|
||||
'color' => '#c5c9c6',
|
||||
];
|
||||
|
||||
return array_merge($defaults, $params ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param string|array|false $settings
|
||||
* @return array|null
|
||||
*/
|
||||
public function panelImage($settings = null): ?array
|
||||
{
|
||||
$defaults = [
|
||||
'ratio' => '3/2',
|
||||
'back' => 'pattern',
|
||||
'cover' => false
|
||||
];
|
||||
|
||||
// switch the image off
|
||||
if ($settings === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($settings) === true) {
|
||||
// use defined icon in blueprint
|
||||
if ($settings === 'icon') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$settings = [
|
||||
'query' => $settings
|
||||
];
|
||||
}
|
||||
|
||||
if ($image = $this->panelImageSource($settings['query'] ?? null)) {
|
||||
|
||||
// main url
|
||||
$settings['url'] = $image->url();
|
||||
|
||||
// for cards
|
||||
$settings['cards'] = [
|
||||
'url' => 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw',
|
||||
'srcset' => $image->srcset([
|
||||
352,
|
||||
864,
|
||||
1408,
|
||||
])
|
||||
];
|
||||
|
||||
// for lists
|
||||
$settings['list'] = [
|
||||
'url' => 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw',
|
||||
'srcset' => $image->srcset([
|
||||
'1x' => [
|
||||
'width' => 38,
|
||||
'height' => 38,
|
||||
'crop' => 'center'
|
||||
],
|
||||
'2x' => [
|
||||
'width' => 76,
|
||||
'height' => 76,
|
||||
'crop' => 'center'
|
||||
],
|
||||
])
|
||||
];
|
||||
|
||||
unset($settings['query']);
|
||||
}
|
||||
|
||||
return array_merge($defaults, (array)$settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image file object based on provided query
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $query
|
||||
* @return \Kirby\Cms\File|\Kirby\Cms\Asset|null
|
||||
*/
|
||||
protected function panelImageSource(string $query = null)
|
||||
{
|
||||
$image = $this->query($query ?? null);
|
||||
|
||||
// validate the query result
|
||||
if (is_a($image, 'Kirby\Cms\File') === false && is_a($image, 'Kirby\Cms\Asset') === false) {
|
||||
$image = null;
|
||||
}
|
||||
|
||||
// fallback for files
|
||||
if ($image === null && is_a($this, 'Kirby\Cms\File') === true && $this->isViewable() === true) {
|
||||
$image = $this;
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all actions
|
||||
* that can be performed in the Panel
|
||||
* This also checks for the lock status
|
||||
* @since 3.3.0
|
||||
*
|
||||
* @param array $unlock An array of options that will be force-unlocked
|
||||
* @return array
|
||||
*/
|
||||
public function panelOptions(array $unlock = []): array
|
||||
{
|
||||
$options = $this->permissions()->toArray();
|
||||
|
||||
if ($this->isLocked()) {
|
||||
foreach ($options as $key => $value) {
|
||||
if (in_array($key, $unlock)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[$key] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Must return the permissions object for the model
|
||||
*
|
||||
* @return \Kirby\Cms\ModelPermissions
|
||||
*/
|
||||
abstract public function permissions();
|
||||
|
||||
/**
|
||||
* Creates a string query, starting from the model
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $query
|
||||
* @param string|null $expect
|
||||
* @return mixed
|
||||
*/
|
||||
public function query(string $query = null, string $expect = null)
|
||||
{
|
||||
if ($query === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = Str::query($query, [
|
||||
'kirby' => $this->kirby(),
|
||||
'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(),
|
||||
static::CLASS_ALIAS => $this
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($expect !== null && is_a($result, $expect) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the content from the content file
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $languageCode
|
||||
* @return array
|
||||
*/
|
||||
public function readContent(string $languageCode = null): array
|
||||
{
|
||||
try {
|
||||
return Data::read($this->contentFile($languageCode));
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the model
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
abstract public function root(): ?string;
|
||||
|
||||
/**
|
||||
* Stores the content on disk
|
||||
*
|
||||
* @internal
|
||||
* @param string $languageCode
|
||||
* @param array $data
|
||||
* @param bool $overwrite
|
||||
* @return self
|
||||
*/
|
||||
public function save(array $data = null, string $languageCode = null, bool $overwrite = false)
|
||||
{
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
return $this->saveTranslation($data, $languageCode, $overwrite);
|
||||
} else {
|
||||
return $this->saveContent($data, $overwrite);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the single language content
|
||||
*
|
||||
* @param array|null $data
|
||||
* @param bool $overwrite
|
||||
* @return self
|
||||
*/
|
||||
protected function saveContent(array $data = null, bool $overwrite = false)
|
||||
{
|
||||
// create a clone to avoid modifying the original
|
||||
$clone = $this->clone();
|
||||
|
||||
// merge the new data with the existing content
|
||||
$clone->content()->update($data, $overwrite);
|
||||
|
||||
// send the full content array to the writer
|
||||
$clone->writeContent($clone->content()->toArray());
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a translation
|
||||
*
|
||||
* @param array|null $data
|
||||
* @param string|null $languageCode
|
||||
* @param bool $overwrite
|
||||
* @return self
|
||||
*/
|
||||
protected function saveTranslation(array $data = null, string $languageCode = null, bool $overwrite = false)
|
||||
{
|
||||
// create a clone to not touch the original
|
||||
$clone = $this->clone();
|
||||
|
||||
// fetch the matching translation and update all the strings
|
||||
$translation = $clone->translation($languageCode);
|
||||
|
||||
if ($translation === null) {
|
||||
throw new InvalidArgumentException('Invalid language: ' . $languageCode);
|
||||
}
|
||||
|
||||
// get the content to store
|
||||
$content = $translation->update($data, $overwrite)->content();
|
||||
$kirby = $this->kirby();
|
||||
$languageCode = $kirby->languageCode($languageCode);
|
||||
|
||||
// remove all untranslatable fields
|
||||
if ($languageCode !== $kirby->defaultLanguage()->code()) {
|
||||
foreach ($this->blueprint()->fields() as $field) {
|
||||
if (($field['translate'] ?? true) === false) {
|
||||
$content[$field['name']] = null;
|
||||
}
|
||||
}
|
||||
|
||||
// merge the translation with the new data
|
||||
$translation->update($content, true);
|
||||
}
|
||||
|
||||
// send the full translation array to the writer
|
||||
$clone->writeContent($translation->content(), $languageCode);
|
||||
|
||||
// reset the content object
|
||||
$clone->content = null;
|
||||
|
||||
// return the updated model
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content object
|
||||
*
|
||||
* @param array|null $content
|
||||
* @return self
|
||||
*/
|
||||
protected function setContent(array $content = null)
|
||||
{
|
||||
if ($content !== null) {
|
||||
$content = new Content($content, $this);
|
||||
}
|
||||
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the translations collection from an array
|
||||
*
|
||||
* @param array $translations
|
||||
* @return self
|
||||
*/
|
||||
protected function setTranslations(array $translations = null)
|
||||
{
|
||||
if ($translations !== null) {
|
||||
$this->translations = new Collection();
|
||||
|
||||
foreach ($translations as $props) {
|
||||
$props['parent'] = $this;
|
||||
$translation = new ContentTranslation($props);
|
||||
$this->translations->data[$translation->code()] = $translation;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* String template builder
|
||||
*
|
||||
* @param string|null $template
|
||||
* @param array $data
|
||||
* @param string $fallback Fallback for tokens in the template that cannot be replaced
|
||||
* @return string
|
||||
*/
|
||||
public function toString(string $template = null, array $data = [], string $fallback = ''): string
|
||||
{
|
||||
if ($template === null) {
|
||||
return $this->id();
|
||||
}
|
||||
|
||||
$result = Str::template($template, array_replace([
|
||||
'kirby' => $this->kirby(),
|
||||
'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(),
|
||||
static::CLASS_ALIAS => $this
|
||||
], $data), $fallback);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single translation by language code
|
||||
* If no code is specified the current translation is returned
|
||||
*
|
||||
* @param string|null $languageCode
|
||||
* @return \Kirby\Cms\ContentTranslation|null
|
||||
*/
|
||||
public function translation(string $languageCode = null)
|
||||
{
|
||||
return $this->translations()->find($languageCode ?? $this->kirby()->language()->code());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translations collection
|
||||
*
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
if ($this->translations !== null) {
|
||||
return $this->translations;
|
||||
}
|
||||
|
||||
$this->translations = new Collection();
|
||||
|
||||
foreach ($this->kirby()->languages() as $language) {
|
||||
$translation = new ContentTranslation([
|
||||
'parent' => $this,
|
||||
'code' => $language->code(),
|
||||
]);
|
||||
|
||||
$this->translations->data[$translation->code()] = $translation;
|
||||
}
|
||||
|
||||
return $this->translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the model data
|
||||
*
|
||||
* @param array $input
|
||||
* @param string $languageCode
|
||||
* @param bool $validate
|
||||
* @return self
|
||||
*/
|
||||
public function update(array $input = null, string $languageCode = null, bool $validate = false)
|
||||
{
|
||||
$form = Form::for($this, [
|
||||
'ignoreDisabled' => $validate === false,
|
||||
'input' => $input,
|
||||
'language' => $languageCode,
|
||||
]);
|
||||
|
||||
// validate the input
|
||||
if ($validate === true) {
|
||||
if ($form->isInvalid() === true) {
|
||||
throw new InvalidArgumentException([
|
||||
'fallback' => 'Invalid form with errors',
|
||||
'details' => $form->errors()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level data writer method
|
||||
* to store the given data on disk or anywhere else
|
||||
*
|
||||
* @internal
|
||||
* @param array $data
|
||||
* @param string $languageCode
|
||||
* @return bool
|
||||
*/
|
||||
public function writeContent(array $data, string $languageCode = null): bool
|
||||
{
|
||||
return Data::write(
|
||||
$this->contentFile($languageCode),
|
||||
$this->contentFileData($data, $languageCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
43
kirby/src/Cms/Nest.php
Normal file
43
kirby/src/Cms/Nest.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The Nest class converts any array type
|
||||
* into a Kirby style collection/object. This
|
||||
* can be used make any type of array compatible
|
||||
* with Kirby queries.
|
||||
*
|
||||
* REFACTOR: move this to the toolkit
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Nest
|
||||
{
|
||||
public static function create($data, $parent = null)
|
||||
{
|
||||
if (is_scalar($data) === true) {
|
||||
return new Field($parent, $data, $data);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value) === true) {
|
||||
$result[$key] = static::create($value, $parent);
|
||||
} elseif (is_scalar($value) === true) {
|
||||
$result[$key] = new Field($parent, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_int(key($data))) {
|
||||
return new NestCollection($result);
|
||||
} else {
|
||||
return new NestObject($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
33
kirby/src/Cms/NestCollection.php
Normal file
33
kirby/src/Cms/NestCollection.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Toolkit\Collection as BaseCollection;
|
||||
|
||||
/**
|
||||
* NestCollection
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
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 $map
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Closure $map = null): array
|
||||
{
|
||||
return parent::toArray($map ?? function ($object) {
|
||||
return $object->toArray();
|
||||
});
|
||||
}
|
||||
}
|
||||
43
kirby/src/Cms/NestObject.php
Normal file
43
kirby/src/Cms/NestObject.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Obj;
|
||||
|
||||
/**
|
||||
* NestObject
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
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) {
|
||||
$result[$key] = $value->value();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_object($value) === true && method_exists($value, 'toArray')) {
|
||||
$result[$key] = $value->toArray();
|
||||
continue;
|
||||
}
|
||||
|
||||
$result[$key] = $value;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
1597
kirby/src/Cms/Page.php
Normal file
1597
kirby/src/Cms/Page.php
Normal file
File diff suppressed because it is too large
Load diff
812
kirby/src/Cms/PageActions.php
Normal file
812
kirby/src/Cms/PageActions.php
Normal file
|
|
@ -0,0 +1,812 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* PageActions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait PageActions
|
||||
{
|
||||
/**
|
||||
* Changes the sorting number
|
||||
* The sorting number must already be correct
|
||||
* when the method is called
|
||||
*
|
||||
* @param int $num
|
||||
* @return self
|
||||
*/
|
||||
public function changeNum(int $num = null)
|
||||
{
|
||||
if ($this->isDraft() === true) {
|
||||
throw new LogicException('Drafts cannot change their sorting number');
|
||||
}
|
||||
|
||||
// don't run the action if everything stayed the same
|
||||
if ($this->num() === $num) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->commit('changeNum', ['page' => $this, 'num' => $num], function ($oldPage, $num) {
|
||||
$newPage = $oldPage->clone([
|
||||
'num' => $num,
|
||||
'dirname' => null,
|
||||
'root' => null
|
||||
]);
|
||||
|
||||
// actually move the page on disk
|
||||
if ($oldPage->exists() === true) {
|
||||
if (Dir::move($oldPage->root(), $newPage->root()) === true) {
|
||||
// Updates the root path of the old page with the root path
|
||||
// of the moved new page to use fly actions on old page in loop
|
||||
$oldPage->setRoot($newPage->root());
|
||||
} else {
|
||||
throw new LogicException('The page directory cannot be moved');
|
||||
}
|
||||
}
|
||||
|
||||
// overwrite the child in the parent page
|
||||
$newPage
|
||||
->parentModel()
|
||||
->children()
|
||||
->set($newPage->id(), $newPage);
|
||||
|
||||
return $newPage;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the slug/uid of the page
|
||||
*
|
||||
* @param string $slug
|
||||
* @param string $languageCode
|
||||
* @return self
|
||||
*/
|
||||
public function changeSlug(string $slug, string $languageCode = null)
|
||||
{
|
||||
// always sanitize the slug
|
||||
$slug = Str::slug($slug);
|
||||
|
||||
// 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 the slug stays exactly the same,
|
||||
// nothing needs to be done.
|
||||
if ($slug === $this->slug()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => null];
|
||||
return $this->commit('changeSlug', $arguments, function ($oldPage, $slug) {
|
||||
$newPage = $oldPage->clone([
|
||||
'slug' => $slug,
|
||||
'dirname' => null,
|
||||
'root' => null
|
||||
]);
|
||||
|
||||
if ($oldPage->exists() === true) {
|
||||
// remove the lock of the old page
|
||||
if ($lock = $oldPage->lock()) {
|
||||
$lock->remove();
|
||||
}
|
||||
|
||||
// actually move stuff on disk
|
||||
if (Dir::move($oldPage->root(), $newPage->root()) !== true) {
|
||||
throw new LogicException('The page directory cannot be moved');
|
||||
}
|
||||
|
||||
// remove from the siblings
|
||||
$oldPage->parentModel()->children()->remove($oldPage);
|
||||
|
||||
Dir::remove($oldPage->mediaRoot());
|
||||
}
|
||||
|
||||
// overwrite the new page in the parent collection
|
||||
if ($newPage->isDraft() === true) {
|
||||
$newPage->parentModel()->drafts()->set($newPage->id(), $newPage);
|
||||
} else {
|
||||
$newPage->parentModel()->children()->set($newPage->id(), $newPage);
|
||||
}
|
||||
|
||||
return $newPage;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the slug for a specific language
|
||||
*
|
||||
* @param string $slug
|
||||
* @param string $languageCode
|
||||
* @return self
|
||||
*/
|
||||
protected function changeSlugForLanguage(string $slug, string $languageCode = null)
|
||||
{
|
||||
$language = $this->kirby()->language($languageCode);
|
||||
|
||||
if (!$language) {
|
||||
throw new NotFoundException('The language: "' . $languageCode . '" does not exist');
|
||||
}
|
||||
|
||||
if ($language->isDefault() === true) {
|
||||
throw new InvalidArgumentException('Use the changeSlug method to change the slug for the default language');
|
||||
}
|
||||
|
||||
$arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $languageCode];
|
||||
return $this->commit('changeSlug', $arguments, function ($page, $slug, $languageCode) {
|
||||
// remove the slug if it's the same as the folder name
|
||||
if ($slug === $page->uid()) {
|
||||
$slug = null;
|
||||
}
|
||||
|
||||
return $page->save(['slug' => $slug], $languageCode);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of the current page
|
||||
* to either draft, listed or unlisted
|
||||
*
|
||||
* @param string $status "draft", "listed" or "unlisted"
|
||||
* @param int $position Optional sorting number
|
||||
* @return self
|
||||
*/
|
||||
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 Exception('Invalid status: ' . $status);
|
||||
}
|
||||
}
|
||||
|
||||
protected function changeStatusToDraft()
|
||||
{
|
||||
$arguments = ['page' => $this, 'status' => 'draft', 'position' => null];
|
||||
$page = $this->commit('changeStatus', $arguments, function ($page) {
|
||||
return $page->unpublish();
|
||||
});
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $position
|
||||
* @return self
|
||||
*/
|
||||
protected function changeStatusToListed(int $position = null)
|
||||
{
|
||||
// create a sorting number for the page
|
||||
$num = $this->createNum($position);
|
||||
|
||||
// don't sort if not necessary
|
||||
if ($this->status() === 'listed' && $num === $this->num()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$arguments = ['page' => $this, 'status' => 'listed', 'position' => $num];
|
||||
$page = $this->commit('changeStatus', $arguments, function ($page, $status, $position) {
|
||||
return $page->publish()->changeNum($position);
|
||||
});
|
||||
|
||||
if ($this->blueprint()->num() === 'default') {
|
||||
$page->resortSiblingsAfterListing($num);
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
protected function changeStatusToUnlisted()
|
||||
{
|
||||
if ($this->status() === 'unlisted') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$arguments = ['page' => $this, 'status' => 'unlisted', 'position' => null];
|
||||
$page = $this->commit('changeStatus', $arguments, function ($page) {
|
||||
return $page->publish()->changeNum(null);
|
||||
});
|
||||
|
||||
$this->resortSiblingsAfterUnlisting();
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the page template
|
||||
*
|
||||
* @param string $template
|
||||
* @return self
|
||||
*/
|
||||
public function changeTemplate(string $template)
|
||||
{
|
||||
if ($template === $this->intendedTemplate()->name()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->commit('changeTemplate', ['page' => $this, 'template' => $template], function ($oldPage, $template) {
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
$newPage = $this->clone([
|
||||
'template' => $template
|
||||
]);
|
||||
|
||||
foreach ($this->kirby()->languages()->codes() as $code) {
|
||||
$content = $oldPage->content($code)->convertTo($template);
|
||||
|
||||
if (F::remove($oldPage->contentFile($code)) !== true) {
|
||||
throw new LogicException('The old text file could not be removed');
|
||||
}
|
||||
|
||||
// save the language file
|
||||
$newPage->save($content, $code);
|
||||
}
|
||||
|
||||
// return a fresh copy of the object
|
||||
return $newPage->clone();
|
||||
} else {
|
||||
$newPage = $this->clone([
|
||||
'content' => $this->content()->convertTo($template),
|
||||
'template' => $template
|
||||
]);
|
||||
|
||||
if (F::remove($oldPage->contentFile()) !== true) {
|
||||
throw new LogicException('The old text file could not be removed');
|
||||
}
|
||||
|
||||
return $newPage->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page title
|
||||
*
|
||||
* @param string $title
|
||||
* @param string|null $languageCode
|
||||
* @return self
|
||||
*/
|
||||
public function changeTitle(string $title, string $languageCode = null)
|
||||
{
|
||||
$arguments = ['page' => $this, 'title' => $title, 'languageCode' => $languageCode];
|
||||
return $this->commit('changeTitle', $arguments, function ($page, $title, $languageCode) {
|
||||
return $page->save(['title' => $title], $languageCode);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits a page action, by following these steps
|
||||
*
|
||||
* 1. checks the action rules
|
||||
* 2. sends the before hook
|
||||
* 3. commits the store action
|
||||
* 4. sends the after hook
|
||||
* 5. returns the result
|
||||
*
|
||||
* @param string $action
|
||||
* @param array $arguments
|
||||
* @param Closure $callback
|
||||
* @return mixed
|
||||
*/
|
||||
protected function commit(string $action, array $arguments, Closure $callback)
|
||||
{
|
||||
$old = $this->hardcopy();
|
||||
$kirby = $this->kirby();
|
||||
$argumentValues = array_values($arguments);
|
||||
|
||||
$this->rules()->$action(...$argumentValues);
|
||||
$kirby->trigger('page.' . $action . ':before', $arguments);
|
||||
|
||||
$result = $callback(...$argumentValues);
|
||||
|
||||
if ($action === 'create') {
|
||||
$argumentsAfter = ['page' => $result];
|
||||
} elseif ($action === 'duplicate') {
|
||||
$argumentsAfter = ['duplicatePage' => $result];
|
||||
} elseif ($action === 'delete') {
|
||||
$argumentsAfter = ['status' => $result, 'page' => $old];
|
||||
} else {
|
||||
$argumentsAfter = ['newPage' => $result, 'oldPage' => $old];
|
||||
}
|
||||
$kirby->trigger('page.' . $action . ':after', $argumentsAfter);
|
||||
|
||||
$kirby->cache('pages')->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the page to a new parent
|
||||
*
|
||||
* @param array $options
|
||||
* @return \Kirby\Cms\Page
|
||||
*/
|
||||
public function copy(array $options = [])
|
||||
{
|
||||
$slug = $options['slug'] ?? $this->slug();
|
||||
$isDraft = $options['isDraft'] ?? $this->isDraft();
|
||||
$parent = $options['parent'] ?? null;
|
||||
$parentModel = $options['parent'] ?? $this->site();
|
||||
$num = $options['num'] ?? null;
|
||||
$children = $options['children'] ?? false;
|
||||
$files = $options['files'] ?? false;
|
||||
|
||||
// clean up the slug
|
||||
$slug = Str::slug($slug);
|
||||
|
||||
if ($parentModel->findPageOrDraft($slug)) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.duplicate',
|
||||
'data' => [
|
||||
'slug' => $slug
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$tmp = new static([
|
||||
'isDraft' => $isDraft,
|
||||
'num' => $num,
|
||||
'parent' => $parent,
|
||||
'slug' => $slug,
|
||||
]);
|
||||
|
||||
$ignore = [
|
||||
$this->kirby()->locks()->file($this)
|
||||
];
|
||||
|
||||
// don't copy files
|
||||
if ($files === false) {
|
||||
foreach ($this->files() as $file) {
|
||||
$ignore[] = $file->root();
|
||||
|
||||
// append all content files
|
||||
array_push($ignore, ...$file->contentFiles());
|
||||
}
|
||||
}
|
||||
|
||||
Dir::copy($this->root(), $tmp->root(), $children, $ignore);
|
||||
|
||||
$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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add copy to siblings
|
||||
if ($isDraft === true) {
|
||||
$parentModel->drafts()->append($copy->id(), $copy);
|
||||
} else {
|
||||
$parentModel->children()->append($copy->id(), $copy);
|
||||
}
|
||||
|
||||
return $copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and stores a new page
|
||||
*
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public static function create(array $props)
|
||||
{
|
||||
// clean up the slug
|
||||
$props['slug'] = Str::slug($props['slug'] ?? $props['content']['title'] ?? null);
|
||||
$props['template'] = $props['model'] = strtolower($props['template'] ?? 'default');
|
||||
$props['isDraft'] = ($props['draft'] ?? true);
|
||||
|
||||
// create a temporary page object
|
||||
$page = Page::factory($props);
|
||||
|
||||
// create a form for the page
|
||||
$form = Form::for($page, [
|
||||
'values' => $props['content'] ?? []
|
||||
]);
|
||||
|
||||
// inject the content
|
||||
$page = $page->clone(['content' => $form->strings(true)]);
|
||||
|
||||
// run the hooks and creation action
|
||||
$page = $page->commit('create', ['page' => $page, 'input' => $props], function ($page, $props) {
|
||||
|
||||
// always create pages in the default language
|
||||
if ($page->kirby()->multilang() === true) {
|
||||
$languageCode = $page->kirby()->defaultLanguage()->code();
|
||||
} else {
|
||||
$languageCode = null;
|
||||
}
|
||||
|
||||
// write the content file
|
||||
$page = $page->save($page->content()->toArray(), $languageCode);
|
||||
|
||||
// flush the parent cache to get children and drafts right
|
||||
if ($page->isDraft() === true) {
|
||||
$page->parentModel()->drafts()->append($page->id(), $page);
|
||||
} else {
|
||||
$page->parentModel()->children()->append($page->id(), $page);
|
||||
}
|
||||
|
||||
return $page;
|
||||
});
|
||||
|
||||
// publish the new page if a number is given
|
||||
if (isset($props['num']) === true) {
|
||||
$page = $page->changeStatus('listed', $props['num']);
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a child of the current page
|
||||
*
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public function createChild(array $props)
|
||||
{
|
||||
$props = array_merge($props, [
|
||||
'url' => null,
|
||||
'num' => null,
|
||||
'parent' => $this,
|
||||
'site' => $this->site(),
|
||||
]);
|
||||
|
||||
$modelClass = Page::$models[$props['template']] ?? Page::class;
|
||||
return $modelClass::create($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the sorting number for the page
|
||||
* depending on the blueprint settings
|
||||
*
|
||||
* @param int $num
|
||||
* @return int
|
||||
*/
|
||||
public function createNum(int $num = null): int
|
||||
{
|
||||
$mode = $this->blueprint()->num();
|
||||
|
||||
switch ($mode) {
|
||||
case 'zero':
|
||||
return 0;
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
$format = $mode === 'date' ? 'Ymd' : 'YmdHi';
|
||||
$lang = $this->kirby()->defaultLanguage() ?? null;
|
||||
$field = $this->content($lang)->get('date');
|
||||
$date = $field->isEmpty() ? 'now' : $field;
|
||||
return date($format, strtotime($date));
|
||||
break;
|
||||
case 'default':
|
||||
|
||||
$max = $this
|
||||
->parentModel()
|
||||
->children()
|
||||
->listed()
|
||||
->merge($this)
|
||||
->count();
|
||||
|
||||
// default positioning at the end
|
||||
if ($num === null) {
|
||||
$num = $max;
|
||||
}
|
||||
|
||||
// avoid zeros or negative numbers
|
||||
if ($num < 1) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// avoid higher numbers than possible
|
||||
if ($num > $max) {
|
||||
return $max;
|
||||
}
|
||||
|
||||
return $num;
|
||||
default:
|
||||
// get instance with default language
|
||||
$app = $this->kirby()->clone();
|
||||
$app->setCurrentLanguage();
|
||||
|
||||
$template = Str::template($mode, [
|
||||
'kirby' => $app,
|
||||
'page' => $app->page($this->id()),
|
||||
'site' => $app->site(),
|
||||
], '');
|
||||
|
||||
return (int)$template;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the page
|
||||
*
|
||||
* @param bool $force
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(bool $force = false): bool
|
||||
{
|
||||
return $this->commit('delete', ['page' => $this, 'force' => $force], function ($page, $force) {
|
||||
|
||||
// delete all files individually
|
||||
foreach ($page->files() as $file) {
|
||||
$file->delete();
|
||||
}
|
||||
|
||||
// delete all children individually
|
||||
foreach ($page->children() as $child) {
|
||||
$child->delete(true);
|
||||
}
|
||||
|
||||
// actually remove the page from disc
|
||||
if ($page->exists() === true) {
|
||||
|
||||
// delete all public media files
|
||||
Dir::remove($page->mediaRoot());
|
||||
|
||||
// delete the content folder for this page
|
||||
Dir::remove($page->root());
|
||||
|
||||
// if the page is a draft and the _drafts folder
|
||||
// is now empty. clean it up.
|
||||
if ($page->isDraft() === true) {
|
||||
$draftsDir = dirname($page->root());
|
||||
|
||||
if (Dir::isEmpty($draftsDir) === true) {
|
||||
Dir::remove($draftsDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($page->isDraft() === true) {
|
||||
$page->parentModel()->drafts()->remove($page);
|
||||
} else {
|
||||
$page->parentModel()->children()->remove($page);
|
||||
$page->resortSiblingsAfterUnlisting();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicates the page with the given
|
||||
* slug and optionally copies all files
|
||||
*
|
||||
* @param string $slug
|
||||
* @param array $options
|
||||
* @return \Kirby\Cms\Page
|
||||
*/
|
||||
public function duplicate(string $slug = null, array $options = [])
|
||||
{
|
||||
|
||||
// create the slug for the duplicate
|
||||
$slug = Str::slug($slug ?? $this->slug() . '-copy');
|
||||
|
||||
$arguments = ['originalPage' => $this, 'input' => $slug, 'options' => $options];
|
||||
return $this->commit('duplicate', $arguments, function ($page, $slug, $options) {
|
||||
return $this->copy([
|
||||
'parent' => $this->parent(),
|
||||
'slug' => $slug,
|
||||
'isDraft' => true,
|
||||
'files' => $options['files'] ?? false,
|
||||
'children' => $options['children'] ?? false,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function publish()
|
||||
{
|
||||
if ($this->isDraft() === false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$page = $this->clone([
|
||||
'isDraft' => false,
|
||||
'root' => null
|
||||
]);
|
||||
|
||||
// actually do it on disk
|
||||
if ($this->exists() === true) {
|
||||
if (Dir::move($this->root(), $page->root()) !== true) {
|
||||
throw new LogicException('The draft folder cannot be moved');
|
||||
}
|
||||
|
||||
// Get the draft folder and check if there are any other drafts
|
||||
// left. Otherwise delete it.
|
||||
$draftDir = dirname($this->root());
|
||||
|
||||
if (Dir::isEmpty($draftDir) === true) {
|
||||
Dir::remove($draftDir);
|
||||
}
|
||||
}
|
||||
|
||||
// remove the page from the parent drafts and add it to children
|
||||
$page->parentModel()->drafts()->remove($page);
|
||||
$page->parentModel()->children()->append($page->id(), $page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean internal caches
|
||||
* @return self
|
||||
*/
|
||||
public function purge()
|
||||
{
|
||||
$this->blueprint = null;
|
||||
$this->children = null;
|
||||
$this->content = null;
|
||||
$this->drafts = null;
|
||||
$this->files = null;
|
||||
$this->inventory = null;
|
||||
$this->translations = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function resortSiblingsAfterListing(int $position = null): bool
|
||||
{
|
||||
// get all siblings including the current page
|
||||
$siblings = $this
|
||||
->parentModel()
|
||||
->children()
|
||||
->listed()
|
||||
->append($this)
|
||||
->filter(function ($page) {
|
||||
return $page->blueprint()->num() === 'default';
|
||||
});
|
||||
|
||||
// get a non-associative array of ids
|
||||
$keys = $siblings->keys();
|
||||
$index = array_search($this->id(), $keys);
|
||||
|
||||
// if the page is not included in the siblings something went wrong
|
||||
if ($index === false) {
|
||||
throw new LogicException('The page is not included in the sorting index');
|
||||
}
|
||||
|
||||
if ($position > count($keys)) {
|
||||
$position = count($keys);
|
||||
}
|
||||
|
||||
// move the current page number in the array of keys
|
||||
// subtract 1 from the num and the position, because of the
|
||||
// zero-based array keys
|
||||
$sorted = A::move($keys, $index, $position - 1);
|
||||
|
||||
foreach ($sorted as $key => $id) {
|
||||
if ($id === $this->id()) {
|
||||
continue;
|
||||
} else {
|
||||
if ($sibling = $siblings->get($id)) {
|
||||
$sibling->changeNum($key + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$parent = $this->parentModel();
|
||||
$parent->children = $parent->children()->sortBy('num', 'asc');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function resortSiblingsAfterUnlisting(): bool
|
||||
{
|
||||
$index = 0;
|
||||
$parent = $this->parentModel();
|
||||
$siblings = $parent
|
||||
->children()
|
||||
->listed()
|
||||
->not($this)
|
||||
->filter(function ($page) {
|
||||
return $page->blueprint()->num() === 'default';
|
||||
});
|
||||
|
||||
if ($siblings->count() > 0) {
|
||||
foreach ($siblings as $sibling) {
|
||||
$index++;
|
||||
$sibling->changeNum($index);
|
||||
}
|
||||
|
||||
$parent->children = $siblings->sortBy('num', 'asc');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sort($position = null)
|
||||
{
|
||||
return $this->changeStatus('listed', $position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a page from listed or
|
||||
* unlisted to draft.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function unpublish()
|
||||
{
|
||||
if ($this->isDraft() === true) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$page = $this->clone([
|
||||
'isDraft' => true,
|
||||
'num' => null,
|
||||
'dirname' => null,
|
||||
'root' => null
|
||||
]);
|
||||
|
||||
// actually do it on disk
|
||||
if ($this->exists() === true) {
|
||||
if (Dir::move($this->root(), $page->root()) !== true) {
|
||||
throw new LogicException('The page folder cannot be moved to drafts');
|
||||
}
|
||||
}
|
||||
|
||||
// remove the page from the parent children and add it to drafts
|
||||
$page->parentModel()->children()->remove($page);
|
||||
$page->parentModel()->drafts()->append($page->id(), $page);
|
||||
|
||||
$page->resortSiblingsAfterUnlisting();
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the page data
|
||||
*
|
||||
* @param array $input
|
||||
* @param string $language
|
||||
* @param bool $validate
|
||||
* @return self
|
||||
*/
|
||||
public function update(array $input = null, string $language = null, bool $validate = false)
|
||||
{
|
||||
if ($this->isDraft() === true) {
|
||||
$validate = false;
|
||||
}
|
||||
|
||||
$page = parent::update($input, $language, $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) {
|
||||
$page = $page->changeNum($page->createNum());
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
}
|
||||
209
kirby/src/Cms/PageBlueprint.php
Normal file
209
kirby/src/Cms/PageBlueprint.php
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* PageBlueprint
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class PageBlueprint extends Blueprint
|
||||
{
|
||||
/**
|
||||
* Creates a new page blueprint object
|
||||
* with the given props
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
parent::__construct($props);
|
||||
|
||||
// normalize all available page options
|
||||
$this->props['options'] = $this->normalizeOptions(
|
||||
$props['options'] ?? true,
|
||||
// defaults
|
||||
[
|
||||
'changeSlug' => null,
|
||||
'changeStatus' => null,
|
||||
'changeTemplate' => null,
|
||||
'changeTitle' => null,
|
||||
'create' => null,
|
||||
'delete' => null,
|
||||
'duplicate' => null,
|
||||
'read' => null,
|
||||
'preview' => null,
|
||||
'sort' => null,
|
||||
'update' => null,
|
||||
],
|
||||
// aliases (from v2)
|
||||
[
|
||||
'status' => 'changeStatus',
|
||||
'template' => 'changeTemplate',
|
||||
'title' => 'changeTitle',
|
||||
'url' => 'changeSlug',
|
||||
]
|
||||
);
|
||||
|
||||
// normalize the ordering number
|
||||
$this->props['num'] = $this->normalizeNum($props['num'] ?? 'default');
|
||||
|
||||
// normalize the available status array
|
||||
$this->props['status'] = $this->normalizeStatus($props['status'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the page numbering mode
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function num(): string
|
||||
{
|
||||
return $this->props['num'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the ordering number
|
||||
*
|
||||
* @param mixed $num
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeNum($num): string
|
||||
{
|
||||
$aliases = [
|
||||
'0' => 'zero',
|
||||
'sort' => 'default',
|
||||
];
|
||||
|
||||
if (isset($aliases[$num]) === true) {
|
||||
return $aliases[$num];
|
||||
}
|
||||
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the available status options for the page
|
||||
*
|
||||
* @param mixed $status
|
||||
* @return array
|
||||
*/
|
||||
protected function normalizeStatus($status): array
|
||||
{
|
||||
$defaults = [
|
||||
'draft' => [
|
||||
'label' => $this->i18n('page.status.draft'),
|
||||
'text' => $this->i18n('page.status.draft.description'),
|
||||
],
|
||||
'unlisted' => [
|
||||
'label' => $this->i18n('page.status.unlisted'),
|
||||
'text' => $this->i18n('page.status.unlisted.description'),
|
||||
],
|
||||
'listed' => [
|
||||
'label' => $this->i18n('page.status.listed'),
|
||||
'text' => $this->i18n('page.status.listed.description'),
|
||||
]
|
||||
];
|
||||
|
||||
// use the defaults, when the status is not defined
|
||||
if (empty($status) === true) {
|
||||
$status = $defaults;
|
||||
}
|
||||
|
||||
// extend the status definition
|
||||
$status = $this->extend($status);
|
||||
|
||||
// clean up and translate each status
|
||||
foreach ($status as $key => $options) {
|
||||
|
||||
// skip invalid status definitions
|
||||
if (in_array($key, ['draft', 'listed', 'unlisted']) === false || $options === false) {
|
||||
unset($status[$key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($options === true) {
|
||||
$status[$key] = $defaults[$key];
|
||||
continue;
|
||||
}
|
||||
|
||||
// convert everything to a simple array
|
||||
if (is_array($options) === false) {
|
||||
$status[$key] = [
|
||||
'label' => $options,
|
||||
'text' => null
|
||||
];
|
||||
}
|
||||
|
||||
// always make sure to have a proper label
|
||||
if (empty($status[$key]['label']) === true) {
|
||||
$status[$key]['label'] = $defaults[$key]['label'];
|
||||
}
|
||||
|
||||
// also make sure to have the text field set
|
||||
if (isset($status[$key]['text']) === false) {
|
||||
$status[$key]['text'] = null;
|
||||
}
|
||||
|
||||
// translate text and label if necessary
|
||||
$status[$key]['label'] = $this->i18n($status[$key]['label'], $status[$key]['label']);
|
||||
$status[$key]['text'] = $this->i18n($status[$key]['text'], $status[$key]['text']);
|
||||
}
|
||||
|
||||
// the draft status is required
|
||||
if (isset($status['draft']) === false) {
|
||||
$status = ['draft' => $defaults['draft']] + $status;
|
||||
}
|
||||
|
||||
// remove the draft status for the home and error pages
|
||||
if ($this->model->isHomeOrErrorPage() === true) {
|
||||
unset($status['draft']);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the options object
|
||||
* that handles page options and permissions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function options(): array
|
||||
{
|
||||
return $this->props['options'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the preview settings
|
||||
* The preview setting controlls the "Open"
|
||||
* button in the panel and redirects it to a
|
||||
* different URL if necessary.
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
public function preview()
|
||||
{
|
||||
$preview = $this->props['options']['preview'] ?? true;
|
||||
|
||||
if (is_string($preview) === true) {
|
||||
return $this->model->toString($preview);
|
||||
}
|
||||
|
||||
return $preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function status(): array
|
||||
{
|
||||
return $this->props['status'];
|
||||
}
|
||||
}
|
||||
62
kirby/src/Cms/PagePermissions.php
Normal file
62
kirby/src/Cms/PagePermissions.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* PagePermissions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class PagePermissions extends ModelPermissions
|
||||
{
|
||||
protected $category = 'pages';
|
||||
|
||||
protected function canChangeSlug(): bool
|
||||
{
|
||||
return $this->model->isHomeOrErrorPage() !== true;
|
||||
}
|
||||
|
||||
protected function canChangeStatus(): bool
|
||||
{
|
||||
return $this->model->isErrorPage() !== true;
|
||||
}
|
||||
|
||||
protected function canChangeTemplate(): bool
|
||||
{
|
||||
if ($this->model->isHomeOrErrorPage() === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($this->model->blueprints()) <= 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function canDelete(): bool
|
||||
{
|
||||
return $this->model->isHomeOrErrorPage() !== true;
|
||||
}
|
||||
|
||||
protected function canSort(): bool
|
||||
{
|
||||
if ($this->model->isErrorPage() === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->model->isListed() !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->model->blueprint()->num() !== 'default') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
264
kirby/src/Cms/PagePicker.php
Normal file
264
kirby/src/Cms/PagePicker.php
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* The PagePicker class helps to
|
||||
* fetch the right pages and the parent
|
||||
* model for the API calls for the
|
||||
* page picker component in the panel.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class PagePicker extends Picker
|
||||
{
|
||||
/**
|
||||
* @var \Kirby\Cms\Pages
|
||||
*/
|
||||
protected $items;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Pages
|
||||
*/
|
||||
protected $itemsForQuery;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Page|\Kirby\Cms\Site|null
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* Extends the basic defaults
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function defaults(): array
|
||||
{
|
||||
return array_merge(parent::defaults(), [
|
||||
// Page ID of the selected parent. Used to navigate
|
||||
'parent' => null,
|
||||
// enable/disable subpage navigation
|
||||
'subpages' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model object that
|
||||
* is currently selected in the page picker.
|
||||
* It normally starts at the site, but can
|
||||
* also be any subpage. When a query is given
|
||||
* and subpage navigation is deactivated,
|
||||
* there will be no model available at all.
|
||||
*
|
||||
* @return \Kirby\Cms\Page|\Kirby\Cms\Site|null
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
// no subpages navigation = no model
|
||||
if ($this->options['subpages'] === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// the model for queries is a bit more tricky to find
|
||||
if (empty($this->options['query']) === false) {
|
||||
return $this->modelForQuery();
|
||||
}
|
||||
|
||||
return $this->parent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a model object for the given
|
||||
* query, depending on the parent and subpages
|
||||
* options.
|
||||
*
|
||||
* @return \Kirby\Cms\Page|\Kirby\Cms\Site|null
|
||||
*/
|
||||
public function modelForQuery()
|
||||
{
|
||||
if ($this->options['subpages'] === true && empty($this->options['parent']) === false) {
|
||||
return $this->parent();
|
||||
}
|
||||
|
||||
if ($items = $this->items()) {
|
||||
return $items->parent();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns basic information about the
|
||||
* parent model that is currently selected
|
||||
* in the page picker.
|
||||
*
|
||||
* @param \Kirby\Cms\Site|\Kirby\Cms\Page|null
|
||||
* @return array|null
|
||||
*/
|
||||
public function modelToArray($model = null): ?array
|
||||
{
|
||||
if ($model === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// the selected model is the site. there's nothing above
|
||||
if (is_a($model, 'Kirby\Cms\Site') === true) {
|
||||
return [
|
||||
'id' => null,
|
||||
'parent' => null,
|
||||
'title' => $model->title()->value()
|
||||
];
|
||||
}
|
||||
|
||||
// the top-most page has been reached
|
||||
// the missing id indicates that there's nothing above
|
||||
if ($model->id() === $this->start()->id()) {
|
||||
return [
|
||||
'id' => null,
|
||||
'parent' => null,
|
||||
'title' => $model->title()->value()
|
||||
];
|
||||
}
|
||||
|
||||
// the model is a regular page
|
||||
return [
|
||||
'id' => $model->id(),
|
||||
'parent' => $model->parentModel()->id(),
|
||||
'title' => $model->title()->value()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all pages for the picker
|
||||
*
|
||||
* @return \Kirby\Cms\Pages|null
|
||||
*/
|
||||
public function items()
|
||||
{
|
||||
// cache
|
||||
if ($this->items !== null) {
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
// no query? simple parent-based search for pages
|
||||
if (empty($this->options['query']) === true) {
|
||||
$items = $this->itemsForParent();
|
||||
|
||||
// when subpage navigation is enabled, a parent
|
||||
// might be passed in addition to the query.
|
||||
// The parent then takes priority.
|
||||
} elseif ($this->options['subpages'] === true && empty($this->options['parent']) === false) {
|
||||
$items = $this->itemsForParent();
|
||||
|
||||
// search by query
|
||||
} else {
|
||||
$items = $this->itemsForQuery();
|
||||
}
|
||||
|
||||
// filter protected pages
|
||||
$items = $items->filterBy('isReadable', true);
|
||||
|
||||
// search
|
||||
$items = $this->search($items);
|
||||
|
||||
// paginate the result
|
||||
return $this->items = $this->paginate($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for pages by parent
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function itemsForParent()
|
||||
{
|
||||
return $this->parent()->children();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for pages by query string
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function itemsForQuery()
|
||||
{
|
||||
// cache
|
||||
if ($this->itemsForQuery !== null) {
|
||||
return $this->itemsForQuery;
|
||||
}
|
||||
|
||||
$model = $this->options['model'];
|
||||
$items = $model->query($this->options['query']);
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
return $this->itemsForQuery = $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model.
|
||||
* The model will be used to fetch
|
||||
* subpages unless there's a specific
|
||||
* query to find pages instead.
|
||||
*
|
||||
* @return \Kirby\Cms\Page|\Kirby\Cms\Site
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
if ($this->parent !== null) {
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
return $this->parent = $this->kirby->page($this->options['parent']) ?? $this->site;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the top-most model (page or site)
|
||||
* that can be accessed when navigating
|
||||
* through pages.
|
||||
*
|
||||
* @return \Kirby\Cms\Page|\Kirby\Cms\Site
|
||||
*/
|
||||
public function start()
|
||||
{
|
||||
if (empty($this->options['query']) === false) {
|
||||
if ($items = $this->itemsForQuery()) {
|
||||
return $items->parent();
|
||||
}
|
||||
|
||||
return $this->site;
|
||||
}
|
||||
|
||||
return $this->site;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array
|
||||
* with all information for the picker.
|
||||
* This will be passed directly to the API.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = parent::toArray();
|
||||
$array['model'] = $this->modelToArray($this->model());
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
298
kirby/src/Cms/PageRules.php
Normal file
298
kirby/src/Cms/PageRules.php
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Validators for all page actions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class PageRules
|
||||
{
|
||||
public static function changeNum(Page $page, int $num = null): bool
|
||||
{
|
||||
if ($num !== null && $num < 0) {
|
||||
throw new InvalidArgumentException(['key' => 'page.num.invalid']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function changeSlug(Page $page, string $slug): bool
|
||||
{
|
||||
if ($page->permissions()->changeSlug() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.changeSlug.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$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 ($duplicate = $drafts->find($slug)) {
|
||||
if ($duplicate->is($page) === false) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.draft.duplicate',
|
||||
'data' => [
|
||||
'slug' => $slug
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function changeStatus(Page $page, string $status, int $position = null): bool
|
||||
{
|
||||
if (isset($page->blueprint()->status()[$status]) === false) {
|
||||
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']);
|
||||
}
|
||||
}
|
||||
|
||||
public static function changeStatusToDraft(Page $page)
|
||||
{
|
||||
if ($page->permissions()->changeStatus() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.changeStatus.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($page->isHomeOrErrorPage() === true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.changeStatus.toDraft.invalid',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function changeStatusToListed(Page $page, int $position)
|
||||
{
|
||||
// no need to check for status changing permissions,
|
||||
// instead we need to check for sorting permissions
|
||||
if ($page->isListed() === true) {
|
||||
if ($page->isSortable() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.sort.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($page->permissions()->changeStatus() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.changeStatus.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($position !== null && $position < 0) {
|
||||
throw new InvalidArgumentException(['key' => 'page.num.invalid']);
|
||||
}
|
||||
|
||||
if ($page->isDraft() === true && empty($page->errors()) === false) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.changeStatus.incomplete',
|
||||
'details' => $page->errors()
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function changeStatusToUnlisted(Page $page)
|
||||
{
|
||||
if ($page->permissions()->changeStatus() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.changeStatus.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function changeTemplate(Page $page, string $template): bool
|
||||
{
|
||||
if ($page->permissions()->changeTemplate() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.changeTemplate.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (count($page->blueprints()) <= 1) {
|
||||
throw new LogicException([
|
||||
'key' => 'page.changeTemplate.invalid',
|
||||
'data' => ['slug' => $page->slug()]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function changeTitle(Page $page, string $title): bool
|
||||
{
|
||||
if (Str::length($title) === 0) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'page.changeTitle.empty',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($page->permissions()->changeTitle() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.changeTitle.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function create(Page $page): bool
|
||||
{
|
||||
if (Str::length($page->slug()) < 1) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'page.slug.invalid',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($page->exists() === true) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.draft.duplicate',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($page->permissions()->create() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.create.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$siblings = $page->parentModel()->children();
|
||||
$drafts = $page->parentModel()->drafts();
|
||||
$slug = $page->slug();
|
||||
|
||||
if ($duplicate = $siblings->find($slug)) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.duplicate',
|
||||
'data' => ['slug' => $slug]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($duplicate = $drafts->find($slug)) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'page.draft.duplicate',
|
||||
'data' => ['slug' => $slug]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function delete(Page $page, bool $force = false): bool
|
||||
{
|
||||
if ($page->permissions()->delete() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.delete.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if (($page->hasChildren() === true || $page->hasDrafts() === true) && $force === false) {
|
||||
throw new LogicException(['key' => 'page.delete.hasChildren']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function duplicate(Page $page, string $slug, array $options = []): bool
|
||||
{
|
||||
if ($page->permissions()->duplicate() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.duplicate.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function update(Page $page, array $content = []): bool
|
||||
{
|
||||
if ($page->permissions()->update() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'page.update.permission',
|
||||
'data' => [
|
||||
'slug' => $page->slug()
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
229
kirby/src/Cms/PageSiblings.php
Normal file
229
kirby/src/Cms/PageSiblings.php
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* PageSiblings
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait PageSiblings
|
||||
{
|
||||
/**
|
||||
* Checks if there's a next listed
|
||||
* page in the siblings collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextListed($collection = null): bool
|
||||
{
|
||||
return $this->nextListed($collection) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a next unlisted
|
||||
* page in the siblings collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextUnlisted($collection = null): bool
|
||||
{
|
||||
return $this->nextUnlisted($collection) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a previous listed
|
||||
* page in the siblings collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrevListed($collection = null): bool
|
||||
{
|
||||
return $this->prevListed($collection) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a previous unlisted
|
||||
* page in the siblings collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrevUnlisted($collection = null): bool
|
||||
{
|
||||
return $this->prevUnlisted($collection) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next listed page if it exists
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function nextListed($collection = null)
|
||||
{
|
||||
return $this->nextAll($collection)->listed()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next unlisted page if it exists
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function nextUnlisted($collection = null)
|
||||
{
|
||||
return $this->nextAll($collection)->unlisted()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous listed page
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function prevListed($collection = null)
|
||||
{
|
||||
return $this->prevAll($collection)->listed()->last();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous unlisted page
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $collection
|
||||
*
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function prevUnlisted($collection = null)
|
||||
{
|
||||
return $this->prevAll($collection)->unlisted()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Private siblings collector
|
||||
*
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
protected function siblingsCollection()
|
||||
{
|
||||
if ($this->isDraft() === true) {
|
||||
return $this->parentModel()->drafts();
|
||||
} else {
|
||||
return $this->parentModel()->children();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns siblings with the same template
|
||||
*
|
||||
* @param bool $self
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function templateSiblings(bool $self = true)
|
||||
{
|
||||
return $this->siblings($self)->filterBy('intendedTemplate', $this->intendedTemplate()->name());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasNextUnlisted()` instead
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextInvisible(): bool
|
||||
{
|
||||
deprecated('$page->hasNextInvisible() is deprecated, use $page->hasNextUnlisted() instead. $page->hasNextInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasNextUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasNextListed()` instead
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextVisible(): bool
|
||||
{
|
||||
deprecated('$page->hasNextVisible() is deprecated, use $page->hasNextListed() instead. $page->hasNextVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasNextListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasPrevUnlisted()` instead
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrevInvisible(): bool
|
||||
{
|
||||
deprecated('$page->hasPrevInvisible() is deprecated, use $page->hasPrevUnlisted() instead. $page->hasPrevInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasPrevUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasPrevListed()` instead
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrevVisible(): bool
|
||||
{
|
||||
deprecated('$page->hasPrevVisible() is deprecated, use $page->hasPrevListed() instead. $page->hasPrevVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasPrevListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::nextUnlisted()` instead
|
||||
* @return self|null
|
||||
*/
|
||||
public function nextInvisible()
|
||||
{
|
||||
deprecated('$page->nextInvisible() is deprecated, use $page->nextUnlisted() instead. $page->nextInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->nextUnlisted();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::nextListed()` instead
|
||||
* @return self|null
|
||||
*/
|
||||
public function nextVisible()
|
||||
{
|
||||
deprecated('$page->nextVisible() is deprecated, use $page->nextListed() instead. $page->nextVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->nextListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::prevUnlisted()` instead
|
||||
* @return self|null
|
||||
*/
|
||||
public function prevInvisible()
|
||||
{
|
||||
deprecated('$page->prevInvisible() is deprecated, use $page->prevUnlisted() instead. $page->prevInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->prevUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::prevListed()` instead
|
||||
* @return self|null
|
||||
*/
|
||||
public function prevVisible()
|
||||
{
|
||||
deprecated('$page->prevVisible() is deprecated, use $page->prevListed() instead. $page->prevVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->prevListed();
|
||||
}
|
||||
}
|
||||
524
kirby/src/Cms/Pages.php
Normal file
524
kirby/src/Cms/Pages.php
Normal file
|
|
@ -0,0 +1,524 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* The `$pages` object refers to a
|
||||
* collection of pages. The pages in this
|
||||
* collection can have the same or different
|
||||
* parents, they can actually exist as
|
||||
* subfolders in the content folder or be
|
||||
* virtual pages created from a database,
|
||||
* an Excel sheet, any API or any other
|
||||
* source.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Pages extends Collection
|
||||
{
|
||||
/**
|
||||
* Cache for the index
|
||||
*
|
||||
* @var \Kirby\Cms\Pages|null
|
||||
*/
|
||||
protected $index = null;
|
||||
|
||||
/**
|
||||
* All registered pages methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $methods = [];
|
||||
|
||||
/**
|
||||
* Adds a single page or
|
||||
* an entire second collection to the
|
||||
* current collection
|
||||
*
|
||||
* @param mixed $object
|
||||
* @return self
|
||||
*/
|
||||
public function add($object)
|
||||
{
|
||||
// add a page collection
|
||||
if (is_a($object, static::class) === true) {
|
||||
$this->data = array_merge($this->data, $object->data);
|
||||
|
||||
// add a page by id
|
||||
} elseif (is_string($object) === true && $page = page($object)) {
|
||||
$this->__set($page->id(), $page);
|
||||
|
||||
// add a page object
|
||||
} elseif (is_a($object, 'Kirby\Cms\Page') === true) {
|
||||
$this->__set($object->id(), $object);
|
||||
|
||||
// give a useful error message on invalid input
|
||||
} elseif (in_array($object, [null, false, true], true) !== true) {
|
||||
throw new InvalidArgumentException('You must pass a Page object to the Pages collection');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all audio files of all children
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function audio()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'audio');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all children for each page in the array
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function children()
|
||||
{
|
||||
$children = new Pages([], $this->parent);
|
||||
|
||||
foreach ($this->data as $page) {
|
||||
foreach ($page->children() as $childKey => $child) {
|
||||
$children->data[$childKey] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all code files of all children
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function code()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'code');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all documents of all children
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function documents()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'document');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all drafts for all pages in the collection
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function drafts()
|
||||
{
|
||||
$drafts = new Pages([], $this->parent);
|
||||
|
||||
foreach ($this->data as $page) {
|
||||
foreach ($page->drafts() as $draftKey => $draft) {
|
||||
$drafts->data[$draftKey] = $draft;
|
||||
}
|
||||
}
|
||||
|
||||
return $drafts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pages collection from an array of props
|
||||
*
|
||||
* @param array $pages
|
||||
* @param \Kirby\Cms\Model $model
|
||||
* @param bool $draft
|
||||
* @return self
|
||||
*/
|
||||
public static function factory(array $pages, Model $model = null, bool $draft = false)
|
||||
{
|
||||
$model = $model ?? App::instance()->site();
|
||||
$children = new static([], $model);
|
||||
$kirby = $model->kirby();
|
||||
|
||||
if (is_a($model, 'Kirby\Cms\Page') === true) {
|
||||
$parent = $model;
|
||||
$site = $model->site();
|
||||
} else {
|
||||
$parent = null;
|
||||
$site = $model;
|
||||
}
|
||||
|
||||
foreach ($pages as $props) {
|
||||
$props['kirby'] = $kirby;
|
||||
$props['parent'] = $parent;
|
||||
$props['site'] = $site;
|
||||
$props['isDraft'] = $draft;
|
||||
|
||||
$page = Page::factory($props);
|
||||
|
||||
$children->data[$page->id()] = $page;
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all files of all children
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function files()
|
||||
{
|
||||
$files = new Files([], $this->parent);
|
||||
|
||||
foreach ($this->data as $page) {
|
||||
foreach ($page->files() as $fileKey => $file) {
|
||||
$files->data[$fileKey] = $file;
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a page in the collection by id.
|
||||
* This works recursively for children and
|
||||
* children of children, etc.
|
||||
*
|
||||
* @param string|null $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function findById(string $id = null)
|
||||
{
|
||||
// remove trailing or leading slashes
|
||||
$id = trim($id, '/');
|
||||
|
||||
// strip extensions from the id
|
||||
if (strpos($id, '.') !== false) {
|
||||
$info = pathinfo($id);
|
||||
|
||||
if ($info['dirname'] !== '.') {
|
||||
$id = $info['dirname'] . '/' . $info['filename'];
|
||||
} else {
|
||||
$id = $info['filename'];
|
||||
}
|
||||
}
|
||||
|
||||
// try the obvious way
|
||||
if ($page = $this->get($id)) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
$multiLang = App::instance()->multilang();
|
||||
|
||||
if ($multiLang === true && $page = $this->findBy('slug', $id)) {
|
||||
return $page;
|
||||
}
|
||||
|
||||
$start = is_a($this->parent, 'Kirby\Cms\Page') === true ? $this->parent->id() : '';
|
||||
$page = $this->findByIdRecursive($id, $start, $multiLang);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
$path = explode('/', $id);
|
||||
$item = null;
|
||||
$query = $startAt;
|
||||
|
||||
foreach ($path as $key) {
|
||||
$collection = $item ? $item->children() : $this;
|
||||
$query = ltrim($query . '/' . $key, '/');
|
||||
$item = $collection->get($query) ?? null;
|
||||
|
||||
if ($item === null && $multiLang === true) {
|
||||
$item = $collection->findBy('slug', $key);
|
||||
}
|
||||
|
||||
if ($item === null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the specialized find by id method
|
||||
*
|
||||
* @param string|null $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function findByKey(string $key = null)
|
||||
{
|
||||
return $this->findById($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for Pages::findById
|
||||
*
|
||||
* @param string $id
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function findByUri(string $id)
|
||||
{
|
||||
return $this->findById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the currently open page
|
||||
*
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function findOpen()
|
||||
{
|
||||
return $this->findBy('isOpen', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom getter that is able to find
|
||||
* extension pages
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function get($key, $default = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($item = parent::get($key)) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
return App::instance()->extension('pages', $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all images of all children
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function images()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a recursive flat index of all
|
||||
* pages and subpages, etc.
|
||||
*
|
||||
* @param bool $drafts
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function index(bool $drafts = false)
|
||||
{
|
||||
if (is_a($this->index, 'Kirby\Cms\Pages') === true) {
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
$this->index = new Pages([], $this->parent);
|
||||
|
||||
foreach ($this->data as $pageKey => $page) {
|
||||
$this->index->data[$pageKey] = $page;
|
||||
$index = $page->index($drafts);
|
||||
|
||||
if ($index) {
|
||||
foreach ($index as $childKey => $child) {
|
||||
$this->index->data[$childKey] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Pages::unlisted()` instead
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function invisible()
|
||||
{
|
||||
deprecated('$pages->invisible() is deprecated, use $pages->unlisted() instead. $pages->invisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->unlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all listed pages in the collection
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function listed()
|
||||
{
|
||||
return $this->filterBy('isListed', '==', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all unlisted pages in the collection
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function unlisted()
|
||||
{
|
||||
return $this->filterBy('isUnlisted', '==', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Include all given items in the collection
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @return self
|
||||
*/
|
||||
public function merge(...$args)
|
||||
{
|
||||
// merge multiple arguments at once
|
||||
if (count($args) > 1) {
|
||||
$collection = clone $this;
|
||||
foreach ($args as $arg) {
|
||||
$collection = $collection->merge($arg);
|
||||
}
|
||||
return $collection;
|
||||
}
|
||||
|
||||
// merge all parent drafts
|
||||
if ($args[0] === 'drafts') {
|
||||
if ($parent = $this->parent()) {
|
||||
return $this->merge($parent->drafts());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// merge an entire collection
|
||||
if (is_a($args[0], static::class) === true) {
|
||||
$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) {
|
||||
$collection = clone $this;
|
||||
return $collection->set($args[0]->id(), $args[0]);
|
||||
}
|
||||
|
||||
// merge an array
|
||||
if (is_array($args[0]) === true) {
|
||||
$collection = clone $this;
|
||||
foreach ($args[0] as $arg) {
|
||||
$collection = $collection->merge($arg);
|
||||
}
|
||||
return $collection;
|
||||
}
|
||||
|
||||
if (is_string($args[0]) === true) {
|
||||
return $this->merge(App::instance()->site()->find($args[0]));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all pages by excluding the given template
|
||||
* @since 3.3.0
|
||||
*
|
||||
* @param string|array $templates
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function notTemplate($templates)
|
||||
{
|
||||
if (empty($templates) === true) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (is_array($templates) === false) {
|
||||
$templates = [$templates];
|
||||
}
|
||||
|
||||
return $this->filter(function ($page) use ($templates) {
|
||||
return !in_array($page->intendedTemplate()->name(), $templates);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all page numbers
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function nums(): array
|
||||
{
|
||||
return $this->pluck('num');
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns all listed and unlisted pages in the collection
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function published()
|
||||
{
|
||||
return $this->filterBy('isDraft', '==', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all pages by the given template
|
||||
*
|
||||
* @param string|array $templates
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function template($templates)
|
||||
{
|
||||
if (empty($templates) === true) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (is_array($templates) === false) {
|
||||
$templates = [$templates];
|
||||
}
|
||||
|
||||
return $this->filter(function ($page) use ($templates) {
|
||||
return in_array($page->intendedTemplate()->name(), $templates);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all video files of all children
|
||||
*
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public function videos()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'video');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Pages::listed()` instead
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function visible()
|
||||
{
|
||||
deprecated('$pages->visible() is deprecated, use $pages->listed() instead. $pages->visible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->listed();
|
||||
}
|
||||
}
|
||||
179
kirby/src/Cms/Pagination.php
Normal file
179
kirby/src/Cms/Pagination.php
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Http\Uri;
|
||||
use Kirby\Toolkit\Pagination as BasePagination;
|
||||
|
||||
/**
|
||||
* The `$pagination` object divides
|
||||
* a collection of pages, files etc.
|
||||
* into discrete pages consisting of
|
||||
* the number of defined items. The
|
||||
* pagination object can then be used
|
||||
* to navigate between these pages,
|
||||
* create a navigation etc.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Pagination extends BasePagination
|
||||
{
|
||||
/**
|
||||
* Pagination method (param, query, none)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $method;
|
||||
|
||||
/**
|
||||
* The base URL
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Variable name for query strings
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $variable;
|
||||
|
||||
/**
|
||||
* Creates the pagination object. As a new
|
||||
* property you can now pass the base Url.
|
||||
* That Url must be the Url of the first
|
||||
* page of the collection without additional
|
||||
* pagination information/query parameters in it.
|
||||
*
|
||||
* ```php
|
||||
* $pagination = new Pagination([
|
||||
* 'page' => 1,
|
||||
* 'limit' => 10,
|
||||
* 'total' => 120,
|
||||
* 'method' => 'query',
|
||||
* 'variable' => 'p',
|
||||
* 'url' => new Uri('https://getkirby.com/blog')
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$config = $kirby->option('pagination', []);
|
||||
$request = $kirby->request();
|
||||
|
||||
$params['limit'] = $params['limit'] ?? $config['limit'] ?? 20;
|
||||
$params['method'] = $params['method'] ?? $config['method'] ?? 'param';
|
||||
$params['variable'] = $params['variable'] ?? $config['variable'] ?? 'page';
|
||||
|
||||
if (empty($params['url']) === true) {
|
||||
$params['url'] = new Uri($kirby->url('current'), [
|
||||
'params' => $request->params(),
|
||||
'query' => $request->query()->toArray(),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($params['method'] === 'query') {
|
||||
$params['page'] = $params['page'] ?? $params['url']->query()->get($params['variable']);
|
||||
} elseif ($params['method'] === 'param') {
|
||||
$params['page'] = $params['page'] ?? $params['url']->params()->get($params['variable']);
|
||||
}
|
||||
|
||||
parent::__construct($params);
|
||||
|
||||
$this->method = $params['method'];
|
||||
$this->url = $params['url'];
|
||||
$this->variable = $params['variable'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Url for the first page
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function firstPageUrl(): string
|
||||
{
|
||||
return $this->pageUrl(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Url for the last page
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function lastPageUrl(): string
|
||||
{
|
||||
return $this->pageUrl($this->lastPage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Url for the next page.
|
||||
* Returns null if there's no next page.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function nextPageUrl(): ?string
|
||||
{
|
||||
if ($page = $this->nextPage()) {
|
||||
return $this->pageUrl($page);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the current page.
|
||||
* If the `$page` variable is set, the URL
|
||||
* for that page will be returned.
|
||||
*
|
||||
* @param int|null $page
|
||||
* @return string|null
|
||||
*/
|
||||
public function pageUrl(int $page = null): ?string
|
||||
{
|
||||
if ($page === null) {
|
||||
return $this->pageUrl($this->page());
|
||||
}
|
||||
|
||||
$url = clone $this->url;
|
||||
$variable = $this->variable;
|
||||
|
||||
if ($this->hasPage($page) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$pageValue = $page === 1 ? null : $page;
|
||||
|
||||
if ($this->method === 'query') {
|
||||
$url->query->$variable = $pageValue;
|
||||
} elseif ($this->method === 'param') {
|
||||
$url->params->$variable = $pageValue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $url->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Url for the previous page.
|
||||
* Returns null if there's no previous page.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function prevPageUrl(): ?string
|
||||
{
|
||||
if ($page = $this->prevPage()) {
|
||||
return $this->pageUrl($page);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
126
kirby/src/Cms/Panel.php
Normal file
126
kirby/src/Cms/Panel.php
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Http\Uri;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\View;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The Panel class is only responsible to create
|
||||
* a working panel view with all the right URLs
|
||||
* and other panel options. The view template is
|
||||
* located in `kirby/views/panel.php`
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Panel
|
||||
{
|
||||
public static function customCss(App $kirby)
|
||||
{
|
||||
if ($css = $kirby->option('panel.css')) {
|
||||
$asset = asset($css);
|
||||
|
||||
if ($asset->exists() === true) {
|
||||
return $asset->url() . '?' . $asset->modified();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function icons(App $kirby): string
|
||||
{
|
||||
return F::read($kirby->root('kirby') . '/panel/dist/img/icons.svg');
|
||||
}
|
||||
|
||||
/**
|
||||
* Links all dist files in the media folder
|
||||
* and returns the link to the requested asset
|
||||
*
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
* @return bool
|
||||
*/
|
||||
public static function link(App $kirby): bool
|
||||
{
|
||||
$mediaRoot = $kirby->root('media') . '/panel';
|
||||
$panelRoot = $kirby->root('panel') . '/dist';
|
||||
$versionHash = $kirby->versionHash();
|
||||
$versionRoot = $mediaRoot . '/' . $versionHash;
|
||||
|
||||
// check if the version already exists
|
||||
if (is_dir($versionRoot) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// delete the panel folder and all previous versions
|
||||
Dir::remove($mediaRoot);
|
||||
|
||||
// recreate the panel folder
|
||||
Dir::make($mediaRoot, true);
|
||||
|
||||
// create a symlink to the dist folder
|
||||
if (Dir::copy($panelRoot, $versionRoot) !== true) {
|
||||
throw new Exception('Panel assets could not be linked');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the main panel view
|
||||
*
|
||||
* @param \Kirby\Cms\App $kirby
|
||||
* @return \Kirby\Cms\Response
|
||||
*/
|
||||
public static function render(App $kirby)
|
||||
{
|
||||
try {
|
||||
if (static::link($kirby) === true) {
|
||||
usleep(1);
|
||||
go($kirby->url('index') . '/' . $kirby->path());
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
die('The Panel assets cannot be installed properly. ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// get the uri object for the panel url
|
||||
$uri = new Uri($url = $kirby->url('panel'));
|
||||
|
||||
// fetch all plugins
|
||||
$plugins = new PanelPlugins();
|
||||
|
||||
$view = new View($kirby->root('kirby') . '/views/panel.php', [
|
||||
'kirby' => $kirby,
|
||||
'config' => $kirby->option('panel'),
|
||||
'assetUrl' => $kirby->url('media') . '/panel/' . $kirby->versionHash(),
|
||||
'customCss' => static::customCss($kirby),
|
||||
'icons' => static::icons($kirby),
|
||||
'pluginCss' => $plugins->url('css'),
|
||||
'pluginJs' => $plugins->url('js'),
|
||||
'panelUrl' => $uri->path()->toString(true) . '/',
|
||||
'nonce' => $kirby->nonce(),
|
||||
'options' => [
|
||||
'url' => $url,
|
||||
'site' => $kirby->url('index'),
|
||||
'api' => $kirby->url('api'),
|
||||
'csrf' => $kirby->option('api.csrf') ?? csrf(),
|
||||
'translation' => 'en',
|
||||
'debug' => $kirby->option('debug', false),
|
||||
'search' => [
|
||||
'limit' => $kirby->option('panel.search.limit') ?? 10
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
return new Response($view->render());
|
||||
}
|
||||
}
|
||||
108
kirby/src/Cms/PanelPlugins.php
Normal file
108
kirby/src/Cms/PanelPlugins.php
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The PanelPlugins class takes care of collecting
|
||||
* js and css plugin files for the panel and caches
|
||||
* them in the media folder
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class PanelPlugins
|
||||
{
|
||||
/**
|
||||
* Cache of all collected plugin files
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $files;
|
||||
|
||||
/**
|
||||
* Collects and returns the plugin files for all plugins
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function files(): array
|
||||
{
|
||||
if ($this->files !== null) {
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
$this->files = [];
|
||||
|
||||
foreach (App::instance()->plugins() as $plugin) {
|
||||
$this->files[] = $plugin->root() . '/index.css';
|
||||
$this->files[] = $plugin->root() . '/index.js';
|
||||
}
|
||||
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification
|
||||
* of the collected plugin files
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function modified(): int
|
||||
{
|
||||
$files = $this->files();
|
||||
$modified = [0];
|
||||
|
||||
foreach ($files as $file) {
|
||||
$modified[] = F::modified($file);
|
||||
}
|
||||
|
||||
return max($modified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the files from all plugins and concatenate them
|
||||
*
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public function read(string $type): string
|
||||
{
|
||||
$dist = [];
|
||||
|
||||
foreach ($this->files() as $file) {
|
||||
if (F::extension($file) === $type) {
|
||||
if ($content = F::read($file)) {
|
||||
if ($type === 'js') {
|
||||
$content = trim($content);
|
||||
|
||||
// make sure that each plugin is ended correctly
|
||||
if (Str::endsWith($content, ';') === false) {
|
||||
$content .= ';';
|
||||
}
|
||||
}
|
||||
|
||||
$dist[] = $content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode(PHP_EOL . PHP_EOL, $dist);
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute url to the cache file
|
||||
* This is used by the panel to link the plugins
|
||||
*
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public function url(string $type): string
|
||||
{
|
||||
return App::instance()->url('media') . '/plugins/index.' . $type . '?' . $this->modified();
|
||||
}
|
||||
}
|
||||
181
kirby/src/Cms/Permissions.php
Normal file
181
kirby/src/Cms/Permissions.php
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Handles permission definition in each user
|
||||
* blueprint and wraps a couple useful methods
|
||||
* around it to check for available permissions.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Permissions
|
||||
{
|
||||
public static $extendedActions = [];
|
||||
|
||||
protected $actions = [
|
||||
'access' => [
|
||||
'panel' => true,
|
||||
'settings' => true,
|
||||
'site' => true,
|
||||
'users' => true,
|
||||
],
|
||||
'files' => [
|
||||
'changeName' => true,
|
||||
'create' => true,
|
||||
'delete' => true,
|
||||
'read' => true,
|
||||
'replace' => true,
|
||||
'update' => true
|
||||
],
|
||||
'languages' => [
|
||||
'create' => true,
|
||||
'delete' => true
|
||||
],
|
||||
'pages' => [
|
||||
'changeSlug' => true,
|
||||
'changeStatus' => true,
|
||||
'changeTemplate' => true,
|
||||
'changeTitle' => true,
|
||||
'create' => true,
|
||||
'delete' => true,
|
||||
'duplicate' => true,
|
||||
'preview' => true,
|
||||
'read' => true,
|
||||
'sort' => true,
|
||||
'update' => true
|
||||
],
|
||||
'site' => [
|
||||
'changeTitle' => true,
|
||||
'update' => true
|
||||
],
|
||||
'users' => [
|
||||
'changeEmail' => true,
|
||||
'changeLanguage' => true,
|
||||
'changeName' => true,
|
||||
'changePassword' => true,
|
||||
'changeRole' => true,
|
||||
'create' => true,
|
||||
'delete' => true,
|
||||
'update' => true
|
||||
],
|
||||
'user' => [
|
||||
'changeEmail' => true,
|
||||
'changeLanguage' => true,
|
||||
'changeName' => true,
|
||||
'changePassword' => true,
|
||||
'changeRole' => true,
|
||||
'delete' => true,
|
||||
'update' => true
|
||||
]
|
||||
];
|
||||
|
||||
public function __construct($settings = [])
|
||||
{
|
||||
// dynamically register the extended actions
|
||||
foreach (static::$extendedActions as $key => $actions) {
|
||||
if (isset($this->actions[$key]) === true) {
|
||||
throw new InvalidArgumentException('The action ' . $key . ' is already a core action');
|
||||
}
|
||||
|
||||
$this->actions[$key] = $actions;
|
||||
}
|
||||
|
||||
if (is_array($settings) === true) {
|
||||
return $this->setCategories($settings);
|
||||
}
|
||||
|
||||
if (is_bool($settings) === true) {
|
||||
return $this->setAll($settings);
|
||||
}
|
||||
}
|
||||
|
||||
public function for(string $category = null, string $action = null): bool
|
||||
{
|
||||
if ($action === null) {
|
||||
if ($this->hasCategory($category) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->actions[$category];
|
||||
}
|
||||
|
||||
if ($this->hasAction($category, $action) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->actions[$category][$action];
|
||||
}
|
||||
|
||||
protected function hasAction(string $category, string $action): bool
|
||||
{
|
||||
return $this->hasCategory($category) === true && array_key_exists($action, $this->actions[$category]) === true;
|
||||
}
|
||||
|
||||
protected function hasCategory(string $category): bool
|
||||
{
|
||||
return array_key_exists($category, $this->actions) === true;
|
||||
}
|
||||
|
||||
protected function setAction(string $category, string $action, $setting)
|
||||
{
|
||||
// wildcard to overwrite the entire category
|
||||
if ($action === '*') {
|
||||
return $this->setCategory($category, $setting);
|
||||
}
|
||||
|
||||
$this->actions[$category][$action] = $setting;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setAll(bool $setting)
|
||||
{
|
||||
foreach ($this->actions as $categoryName => $actions) {
|
||||
$this->setCategory($categoryName, $setting);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setCategories(array $settings)
|
||||
{
|
||||
foreach ($settings as $categoryName => $categoryActions) {
|
||||
if (is_bool($categoryActions) === true) {
|
||||
$this->setCategory($categoryName, $categoryActions);
|
||||
}
|
||||
|
||||
if (is_array($categoryActions) === true) {
|
||||
foreach ($categoryActions as $actionName => $actionSetting) {
|
||||
$this->setAction($categoryName, $actionName, $actionSetting);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setCategory(string $category, bool $setting)
|
||||
{
|
||||
if ($this->hasCategory($category) === false) {
|
||||
throw new InvalidArgumentException('Invalid permissions category');
|
||||
}
|
||||
|
||||
foreach ($this->actions[$category] as $actionName => $actionSetting) {
|
||||
$this->actions[$category][$actionName] = $setting;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->actions;
|
||||
}
|
||||
}
|
||||
176
kirby/src/Cms/Picker.php
Normal file
176
kirby/src/Cms/Picker.php
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The Picker abstract is the foundation
|
||||
* for the UserPicker, PagePicker and FilePicker
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
abstract class Picker
|
||||
{
|
||||
/**
|
||||
* @var \Kirby\Cms\App
|
||||
*/
|
||||
protected $kirby;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Site
|
||||
*/
|
||||
protected $site;
|
||||
|
||||
/**
|
||||
* Creates a new Picker instance
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->options = array_merge($this->defaults(), $params);
|
||||
$this->kirby = $this->options['model']->kirby();
|
||||
$this->site = $this->kirby->site();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the array of default values
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function defaults(): array
|
||||
{
|
||||
// default params
|
||||
return [
|
||||
// image settings (ratio, cover, etc.)
|
||||
'image' => [],
|
||||
// query template for the info field
|
||||
'info' => false,
|
||||
// number of users displayed per pagination page
|
||||
'limit' => 20,
|
||||
// optional mapping function for the result array
|
||||
'map' => null,
|
||||
// the reference model
|
||||
'model' => site(),
|
||||
// current page when paginating
|
||||
'page' => 1,
|
||||
// a query string to fetch specific items
|
||||
'query' => null,
|
||||
// search query
|
||||
'search' => null,
|
||||
// query template for the text field
|
||||
'text' => null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all items for the picker
|
||||
*
|
||||
* @return \Kirby\Cms\Collection|null
|
||||
*/
|
||||
abstract public function items();
|
||||
|
||||
/**
|
||||
* Converts all given items to an associative
|
||||
* array that is already optimized for the
|
||||
* panel picker component.
|
||||
*
|
||||
* @param \Kirby\Cms\Collection|null $items
|
||||
* @return array
|
||||
*/
|
||||
public function itemsToArray($items = null): array
|
||||
{
|
||||
if ($items === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($items as $index => $item) {
|
||||
if (empty($this->options['map']) === false) {
|
||||
$result[] = $this->options['map']($item);
|
||||
} else {
|
||||
$result[] = $item->panelPickerData([
|
||||
'image' => $this->options['image'],
|
||||
'info' => $this->options['info'],
|
||||
'model' => $this->options['model'],
|
||||
'text' => $this->options['text'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply pagination to the collection
|
||||
* of items according to the options.
|
||||
*
|
||||
* @param \Kirby\Cms\Collection $items
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function paginate($items)
|
||||
{
|
||||
return $items->paginate([
|
||||
'limit' => $this->options['limit'],
|
||||
'page' => $this->options['page']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the most relevant pagination
|
||||
* info as array
|
||||
*
|
||||
* @param \Kirby\Cms\Pagination $pagination
|
||||
* @return array
|
||||
*/
|
||||
public function paginationToArray(Pagination $pagination): array
|
||||
{
|
||||
return [
|
||||
'limit' => $pagination->limit(),
|
||||
'page' => $pagination->page(),
|
||||
'total' => $pagination->total()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through the collection of items
|
||||
* if not deactivate in the options
|
||||
*
|
||||
* @param \Kirby\Cms\Collection $items
|
||||
* @return \Kirby\Cms\Collection
|
||||
*/
|
||||
public function search($items)
|
||||
{
|
||||
if (empty($this->options['search']) === false) {
|
||||
return $items->search($this->options['search']);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an associative array
|
||||
* with all information for the picker.
|
||||
* This will be passed directly to the API.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$items = $this->items();
|
||||
|
||||
return [
|
||||
'data' => $this->itemsToArray($items),
|
||||
'pagination' => $this->paginationToArray($items->pagination()),
|
||||
];
|
||||
}
|
||||
}
|
||||
115
kirby/src/Cms/Plugin.php
Normal file
115
kirby/src/Cms/Plugin.php
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents a Plugin and handles parsing of
|
||||
* the composer.json. It also creates the prefix
|
||||
* and media url for the plugin.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Plugin extends Model
|
||||
{
|
||||
protected $extends;
|
||||
protected $info;
|
||||
protected $name;
|
||||
protected $root;
|
||||
|
||||
public function __call(string $key, array $arguments = null)
|
||||
{
|
||||
return $this->info()[$key] ?? null;
|
||||
}
|
||||
|
||||
public function __construct(string $name, array $extends = [])
|
||||
{
|
||||
$this->setName($name);
|
||||
$this->extends = $extends;
|
||||
$this->root = $extends['root'] ?? dirname(debug_backtrace()[0]['file']);
|
||||
|
||||
unset($this->extends['root']);
|
||||
}
|
||||
|
||||
public function extends(): array
|
||||
{
|
||||
return $this->extends;
|
||||
}
|
||||
|
||||
public function info(): array
|
||||
{
|
||||
if (is_array($this->info) === true) {
|
||||
return $this->info;
|
||||
}
|
||||
|
||||
try {
|
||||
$info = Data::read($this->manifest());
|
||||
} catch (Exception $e) {
|
||||
// there is no manifest file or it is invalid
|
||||
$info = [];
|
||||
}
|
||||
|
||||
return $this->info = $info;
|
||||
}
|
||||
|
||||
public function manifest(): string
|
||||
{
|
||||
return $this->root() . '/composer.json';
|
||||
}
|
||||
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return App::instance()->root('media') . '/plugins/' . $this->name();
|
||||
}
|
||||
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
return App::instance()->url('media') . '/plugins/' . $this->name();
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function option(string $key)
|
||||
{
|
||||
return $this->kirby()->option($this->prefix() . '.' . $key);
|
||||
}
|
||||
|
||||
public function prefix(): string
|
||||
{
|
||||
return str_replace('/', '.', $this->name());
|
||||
}
|
||||
|
||||
public function root(): string
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
protected function setName(string $name)
|
||||
{
|
||||
if (preg_match('!^[a-z0-9-]+\/[a-z0-9-]+$!i', $name) == false) {
|
||||
throw new InvalidArgumentException('The plugin name must follow the format "a-z0-9-/a-z0-9-"');
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->propertiesToArray();
|
||||
}
|
||||
}
|
||||
84
kirby/src/Cms/PluginAssets.php
Normal file
84
kirby/src/Cms/PluginAssets.php
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* Plugin assets are automatically copied/linked
|
||||
* to the media folder, to make them publicly
|
||||
* available. This class handles the magic around that.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class PluginAssets
|
||||
{
|
||||
/**
|
||||
* Clean old/deprecated assets on every resolve
|
||||
*
|
||||
* @param string $pluginName
|
||||
* @return void
|
||||
*/
|
||||
public static function clean(string $pluginName): void
|
||||
{
|
||||
if ($plugin = App::instance()->plugin($pluginName)) {
|
||||
$root = $plugin->root() . '/assets';
|
||||
$media = $plugin->mediaRoot();
|
||||
$assets = Dir::index($media, true);
|
||||
|
||||
foreach ($assets as $asset) {
|
||||
$original = $root . '/' . $asset;
|
||||
|
||||
if (file_exists($original) === false) {
|
||||
$assetRoot = $media . '/' . $asset;
|
||||
|
||||
if (is_file($assetRoot) === true) {
|
||||
F::remove($assetRoot);
|
||||
} else {
|
||||
Dir::remove($assetRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a symlink for a plugin asset and
|
||||
* return the public URL
|
||||
*
|
||||
* @param string $pluginName
|
||||
* @param string $filename
|
||||
* @return \Kirby\Cms\Response|null
|
||||
*/
|
||||
public static function resolve(string $pluginName, string $filename)
|
||||
{
|
||||
if ($plugin = App::instance()->plugin($pluginName)) {
|
||||
$source = $plugin->root() . '/assets/' . $filename;
|
||||
|
||||
if (F::exists($source, $plugin->root()) === true) {
|
||||
// do some spring cleaning for older files
|
||||
static::clean($pluginName);
|
||||
|
||||
$target = $plugin->mediaRoot() . '/' . $filename;
|
||||
$url = $plugin->mediaUrl() . '/' . $filename;
|
||||
|
||||
// create the plugin directory first
|
||||
Dir::make($plugin->mediaRoot(), true);
|
||||
|
||||
if (F::link($source, $target, 'symlink') === true) {
|
||||
return Response::redirect($url);
|
||||
}
|
||||
|
||||
return Response::file($source);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
25
kirby/src/Cms/R.php
Normal file
25
kirby/src/Cms/R.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Facade;
|
||||
|
||||
/**
|
||||
* Shortcut to the request object
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class R extends Facade
|
||||
{
|
||||
/**
|
||||
* @return \Kirby\Http\Request
|
||||
*/
|
||||
public static function instance()
|
||||
{
|
||||
return App::instance()->request();
|
||||
}
|
||||
}
|
||||
222
kirby/src/Cms/Responder.php
Normal file
222
kirby/src/Cms/Responder.php
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Mime;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Global response configuration
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Responder
|
||||
{
|
||||
/**
|
||||
* HTTP status code
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code = null;
|
||||
|
||||
/**
|
||||
* Response body
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $body = null;
|
||||
|
||||
/**
|
||||
* HTTP headers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [];
|
||||
|
||||
/**
|
||||
* Content type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = null;
|
||||
|
||||
/**
|
||||
* Creates and sends the response
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string)$this->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for the response body
|
||||
*
|
||||
* @param string $body
|
||||
* @return string|self
|
||||
*/
|
||||
public function body(string $body = null)
|
||||
{
|
||||
if ($body === null) {
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
$this->body = $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for the status code
|
||||
*
|
||||
* @param int $code
|
||||
* @return int|self
|
||||
*/
|
||||
public function code(int $code = null)
|
||||
{
|
||||
if ($code === null) {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
$this->code = $code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct response from an array
|
||||
*
|
||||
* @param array $response
|
||||
*/
|
||||
public function fromArray(array $response): void
|
||||
{
|
||||
$this->body($response['body'] ?? null);
|
||||
$this->code($response['code'] ?? null);
|
||||
$this->headers($response['headers'] ?? null);
|
||||
$this->type($response['type'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for a single header
|
||||
*
|
||||
* @param string $key
|
||||
* @param string|false|null $value
|
||||
* @return string|self
|
||||
*/
|
||||
public function header(string $key, $value = null)
|
||||
{
|
||||
if ($value === null) {
|
||||
return $this->headers[$key] ?? null;
|
||||
}
|
||||
|
||||
if ($value === false) {
|
||||
unset($this->headers[$key]);
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->headers[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for all headers
|
||||
*
|
||||
* @param array $headers
|
||||
* @return array|self
|
||||
*/
|
||||
public function headers(array $headers = null)
|
||||
{
|
||||
if ($headers === null) {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
$this->headers = $headers;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to configure a json response
|
||||
*
|
||||
* @param array $json
|
||||
* @return string|self
|
||||
*/
|
||||
public function json(array $json = null)
|
||||
{
|
||||
if ($json !== null) {
|
||||
$this->body(json_encode($json));
|
||||
}
|
||||
|
||||
return $this->type('application/json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut to create a redirect response
|
||||
*
|
||||
* @param string|null $location
|
||||
* @param int|null $code
|
||||
* @return self
|
||||
*/
|
||||
public function redirect(?string $location = null, ?int $code = null)
|
||||
{
|
||||
$location = Url::to($location ?? '/');
|
||||
$location = Url::unIdn($location);
|
||||
|
||||
return $this
|
||||
->header('Location', (string)$location)
|
||||
->code($code ?? 302);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns the response object from the config
|
||||
*
|
||||
* @param string|null $body
|
||||
* @return \Kirby\Cms\Response
|
||||
*/
|
||||
public function send(string $body = null)
|
||||
{
|
||||
if ($body !== null) {
|
||||
$this->body($body);
|
||||
}
|
||||
|
||||
return new Response($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the response configuration
|
||||
* to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'body' => $this->body,
|
||||
'code' => $this->code,
|
||||
'headers' => $this->headers,
|
||||
'type' => $this->type,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter and getter for the content type
|
||||
*
|
||||
* @param string $type
|
||||
* @return string|self
|
||||
*/
|
||||
public function type(string $type = null)
|
||||
{
|
||||
if ($type === null) {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
if (Str::contains($type, '/') === false) {
|
||||
$type = Mime::fromExtension($type);
|
||||
}
|
||||
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
30
kirby/src/Cms/Response.php
Normal file
30
kirby/src/Cms/Response.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Custom response object with an optimized
|
||||
* redirect method to build correct Urls
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
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 self
|
||||
*/
|
||||
public static function redirect(?string $location = null, ?int $code = null)
|
||||
{
|
||||
return parent::redirect(Url::to($location ?? '/'), $code);
|
||||
}
|
||||
}
|
||||
204
kirby/src/Cms/Role.php
Normal file
204
kirby/src/Cms/Role.php
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
/**
|
||||
* Represents a User role with attached
|
||||
* permissions. Roles are defined by user blueprints.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Role extends Model
|
||||
{
|
||||
protected $description;
|
||||
protected $name;
|
||||
protected $permissions;
|
||||
protected $title;
|
||||
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name();
|
||||
}
|
||||
|
||||
public static function admin(array $inject = [])
|
||||
{
|
||||
try {
|
||||
return static::load('admin');
|
||||
} catch (Exception $e) {
|
||||
return static::factory(static::defaults()['admin'], $inject);
|
||||
}
|
||||
}
|
||||
|
||||
protected static function defaults(): array
|
||||
{
|
||||
return [
|
||||
'admin' => [
|
||||
'name' => 'admin',
|
||||
'description' => I18n::translate('role.admin.description'),
|
||||
'title' => I18n::translate('role.admin.title'),
|
||||
'permissions' => true,
|
||||
],
|
||||
'nobody' => [
|
||||
'name' => 'nobody',
|
||||
'description' => I18n::translate('role.nobody.description'),
|
||||
'title' => I18n::translate('role.nobody.title'),
|
||||
'permissions' => false,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function description()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $props
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function factory(array $props, array $inject = [])
|
||||
{
|
||||
return new static($props + $inject);
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->name();
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->name() === 'admin';
|
||||
}
|
||||
|
||||
public function isNobody(): bool
|
||||
{
|
||||
return $this->name() === 'nobody';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function load(string $file, array $inject = [])
|
||||
{
|
||||
$data = Data::read($file);
|
||||
$data['name'] = F::name($file);
|
||||
|
||||
return static::factory($data, $inject);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function nobody(array $inject = [])
|
||||
{
|
||||
try {
|
||||
return static::load('nobody');
|
||||
} catch (Exception $e) {
|
||||
return static::factory(static::defaults()['nobody'], $inject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Kirby\Cms\Permissions
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [type] $description
|
||||
* @return self
|
||||
*/
|
||||
protected function setDescription($description = null)
|
||||
{
|
||||
$this->description = I18n::translate($description, $description);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
protected function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [type] $permissions
|
||||
* @return self
|
||||
*/
|
||||
protected function setPermissions($permissions = null)
|
||||
{
|
||||
$this->permissions = new Permissions($permissions);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param [type] $title
|
||||
* @return self
|
||||
*/
|
||||
protected function setTitle($title = null)
|
||||
{
|
||||
$this->title = I18n::translate($title, $title);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return $this->title = $this->title ?? ucfirst($this->name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the most important role
|
||||
* properties to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'description' => $this->description(),
|
||||
'id' => $this->id(),
|
||||
'name' => $this->name(),
|
||||
'permissions' => $this->permissions()->toArray(),
|
||||
'title' => $this->title(),
|
||||
];
|
||||
}
|
||||
}
|
||||
137
kirby/src/Cms/Roles.php
Normal file
137
kirby/src/Cms/Roles.php
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of the Collection class that
|
||||
* introduces `Roles::factory()` to convert an
|
||||
* array of role definitions into a proper
|
||||
* collection with Role objects. It also has
|
||||
* a `Roles::load()` method that handles loading
|
||||
* role definitions from disk.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Roles extends Collection
|
||||
{
|
||||
/**
|
||||
* Returns a filtered list of all
|
||||
* roles that can be created by the
|
||||
* current user
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function canBeChanged()
|
||||
{
|
||||
if (App::instance()->user()) {
|
||||
return $this->filter(function ($role) {
|
||||
$newUser = new User([
|
||||
'email' => 'test@getkirby.com',
|
||||
'role' => $role->id()
|
||||
]);
|
||||
|
||||
return $newUser->permissions()->can('changeRole');
|
||||
});
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a filtered list of all
|
||||
* roles that can be created by the
|
||||
* current user
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function canBeCreated()
|
||||
{
|
||||
if (App::instance()->user()) {
|
||||
return $this->filter(function ($role) {
|
||||
$newUser = new User([
|
||||
'email' => 'test@getkirby.com',
|
||||
'role' => $role->id()
|
||||
]);
|
||||
|
||||
return $newUser->permissions()->can('create');
|
||||
});
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $roles
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function factory(array $roles, array $inject = [])
|
||||
{
|
||||
$collection = new static();
|
||||
|
||||
// read all user blueprints
|
||||
foreach ($roles as $props) {
|
||||
$role = Role::factory($props, $inject);
|
||||
$collection->set($role->id(), $role);
|
||||
}
|
||||
|
||||
// always include the admin role
|
||||
if ($collection->find('admin') === null) {
|
||||
$collection->set('admin', Role::admin());
|
||||
}
|
||||
|
||||
// return the collection sorted by name
|
||||
return $collection->sortBy('name', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $root
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function load(string $root = null, array $inject = [])
|
||||
{
|
||||
$roles = new static();
|
||||
|
||||
// load roles from plugins
|
||||
foreach (App::instance()->extensions('blueprints') as $blueprintName => $blueprint) {
|
||||
if (substr($blueprintName, 0, 6) !== 'users/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($blueprint) === true) {
|
||||
$role = Role::factory($blueprint, $inject);
|
||||
} else {
|
||||
$role = Role::load($blueprint, $inject);
|
||||
}
|
||||
|
||||
$roles->set($role->id(), $role);
|
||||
}
|
||||
|
||||
// load roles from directory
|
||||
if ($root !== null) {
|
||||
foreach (glob($root . '/*.yml') as $file) {
|
||||
$filename = basename($file);
|
||||
|
||||
if ($filename === 'default.yml') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$role = Role::load($file, $inject);
|
||||
$roles->set($role->id(), $role);
|
||||
}
|
||||
}
|
||||
|
||||
// always include the admin role
|
||||
if ($roles->find('admin') === null) {
|
||||
$roles->set('admin', Role::admin($inject));
|
||||
}
|
||||
|
||||
// return the collection sorted by name
|
||||
return $roles->sortBy('name', 'asc');
|
||||
}
|
||||
}
|
||||
26
kirby/src/Cms/S.php
Normal file
26
kirby/src/Cms/S.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Session\Session;
|
||||
use Kirby\Toolkit\Facade;
|
||||
|
||||
/**
|
||||
* Shortcut to the session object
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class S extends Facade
|
||||
{
|
||||
/**
|
||||
* @return \Kirby\Session\Session
|
||||
*/
|
||||
public static function instance()
|
||||
{
|
||||
return App::instance()->session();
|
||||
}
|
||||
}
|
||||
62
kirby/src/Cms/Search.php
Normal file
62
kirby/src/Cms/Search.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The Search class extracts the
|
||||
* search logic from collections, to
|
||||
* provide a more globally usable interface
|
||||
* for any searches.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Search
|
||||
{
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $params
|
||||
* @return \Kirby\Cms\Files
|
||||
*/
|
||||
public static function files(string $query = null, $params = [])
|
||||
{
|
||||
return App::instance()->site()->index()->files()->search($query, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Native search method to search for anything within the collection
|
||||
*
|
||||
* @param \Kirby\Cms\Collection $collection
|
||||
* @param string $query
|
||||
* @param mixed $params
|
||||
* @return \Kirby\Cms\Collection|bool
|
||||
*/
|
||||
public static function collection(Collection $collection, string $query = null, $params = [])
|
||||
{
|
||||
$kirby = App::instance();
|
||||
return $kirby->component('search')($kirby, $collection, $query, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $params
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public static function pages(string $query = null, $params = [])
|
||||
{
|
||||
return App::instance()->site()->index()->search($query, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $query
|
||||
* @param array $params
|
||||
* @return \Kirby\Cms\Users
|
||||
*/
|
||||
public static function users(string $query = null, $params = [])
|
||||
{
|
||||
return App::instance()->users()->search($query, $params);
|
||||
}
|
||||
}
|
||||
81
kirby/src/Cms/Section.php
Normal file
81
kirby/src/Cms/Section.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Component;
|
||||
|
||||
/**
|
||||
* Section
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Section extends Component
|
||||
{
|
||||
/**
|
||||
* Registry for all component mixins
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $mixins = [];
|
||||
|
||||
/**
|
||||
* Registry for all component types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $types = [];
|
||||
|
||||
|
||||
public function __construct(string $type, array $attrs = [])
|
||||
{
|
||||
if (isset($attrs['model']) === false) {
|
||||
throw new InvalidArgumentException('Undefined section model');
|
||||
}
|
||||
|
||||
// use the type as fallback for the name
|
||||
$attrs['name'] = $attrs['name'] ?? $type;
|
||||
$attrs['type'] = $type;
|
||||
|
||||
parent::__construct($type, $attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
{
|
||||
return $this->model->kirby();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Kirby\Cms\Model
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = parent::toArray();
|
||||
|
||||
unset($array['model']);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
public function toResponse(): array
|
||||
{
|
||||
return array_merge([
|
||||
'status' => 'ok',
|
||||
'code' => 200,
|
||||
'name' => $this->name,
|
||||
'type' => $this->type
|
||||
], $this->toArray());
|
||||
}
|
||||
}
|
||||
688
kirby/src/Cms/Site.php
Normal file
688
kirby/src/Cms/Site.php
Normal file
|
|
@ -0,0 +1,688 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
/**
|
||||
* The `$site` object is the root element
|
||||
* for any site with pages. It represents
|
||||
* the main content folder with its
|
||||
* `site.txt`.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Site extends ModelWithContent
|
||||
{
|
||||
const CLASS_ALIAS = 'site';
|
||||
|
||||
use SiteActions;
|
||||
use HasChildren;
|
||||
use HasFiles;
|
||||
use HasMethods;
|
||||
|
||||
/**
|
||||
* The SiteBlueprint object
|
||||
*
|
||||
* @var SiteBlueprint
|
||||
*/
|
||||
protected $blueprint;
|
||||
|
||||
/**
|
||||
* The error page object
|
||||
*
|
||||
* @var Page
|
||||
*/
|
||||
protected $errorPage;
|
||||
|
||||
/**
|
||||
* The id of the error page, which is
|
||||
* fetched in the errorPage method
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $errorPageId = 'error';
|
||||
|
||||
/**
|
||||
* The home page object
|
||||
*
|
||||
* @var Page
|
||||
*/
|
||||
protected $homePage;
|
||||
|
||||
/**
|
||||
* The id of the home page, which is
|
||||
* fetched in the errorPage method
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $homePageId = 'home';
|
||||
|
||||
/**
|
||||
* Cache for the inventory array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $inventory;
|
||||
|
||||
/**
|
||||
* The current page object
|
||||
*
|
||||
* @var Page
|
||||
*/
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* The absolute path to the site directory
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* The page url
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* Modified getter to also return fields
|
||||
* from the content
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
// public property access
|
||||
if (isset($this->$method) === true) {
|
||||
return $this->$method;
|
||||
}
|
||||
|
||||
// site methods
|
||||
if ($this->hasMethod($method)) {
|
||||
return $this->callMethod($method, $arguments);
|
||||
}
|
||||
|
||||
// return site content otherwise
|
||||
return $this->content()->get($method, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Site object
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props = [])
|
||||
{
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return array_merge($this->toArray(), [
|
||||
'content' => $this->content(),
|
||||
'children' => $this->children(),
|
||||
'files' => $this->files(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the api endpoint
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
*/
|
||||
public function apiUrl(bool $relative = false): string
|
||||
{
|
||||
if ($relative === true) {
|
||||
return 'site';
|
||||
} else {
|
||||
return $this->kirby()->url('api') . '/site';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the blueprint object
|
||||
*
|
||||
* @return \Kirby\Cms\SiteBlueprint
|
||||
*/
|
||||
public function blueprint()
|
||||
{
|
||||
if (is_a($this->blueprint, 'Kirby\Cms\SiteBlueprint') === true) {
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
return $this->blueprint = SiteBlueprint::factory('site', null, $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all blueprints that are available
|
||||
* as subpages of the site
|
||||
*
|
||||
* @param string $inSection
|
||||
* @return array
|
||||
*/
|
||||
public function blueprints(string $inSection = null): array
|
||||
{
|
||||
$blueprints = [];
|
||||
$blueprint = $this->blueprint();
|
||||
$sections = $inSection !== null ? [$blueprint->section($inSection)] : $blueprint->sections();
|
||||
|
||||
foreach ($sections as $section) {
|
||||
if ($section === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ((array)$section->blueprints() as $blueprint) {
|
||||
$blueprints[$blueprint['name']] = $blueprint;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($blueprints);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a breadcrumb collection
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function breadcrumb()
|
||||
{
|
||||
// get all parents and flip the order
|
||||
$crumb = $this->page()->parents()->flip();
|
||||
|
||||
// add the home page
|
||||
$crumb->prepend($this->homePage()->id(), $this->homePage());
|
||||
|
||||
// add the active page
|
||||
$crumb->append($this->page()->id(), $this->page());
|
||||
|
||||
return $crumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the content for the write method
|
||||
*
|
||||
* @internal
|
||||
* @param array $data
|
||||
* @param string $languageCode
|
||||
* @return array
|
||||
*/
|
||||
public function contentFileData(array $data, string $languageCode = null): array
|
||||
{
|
||||
return A::prepend($data, [
|
||||
'title' => $data['title'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename for the content file
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileName(): string
|
||||
{
|
||||
return 'site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error page object
|
||||
*
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function errorPage()
|
||||
{
|
||||
if (is_a($this->errorPage, 'Kirby\Cms\Page') === true) {
|
||||
return $this->errorPage;
|
||||
}
|
||||
|
||||
if ($error = $this->find($this->errorPageId())) {
|
||||
return $this->errorPage = $error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the global error page id
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function errorPageId(): string
|
||||
{
|
||||
return $this->errorPageId ?? 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the site exists on disk
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return is_dir($this->root()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the home page object
|
||||
*
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function homePage()
|
||||
{
|
||||
if (is_a($this->homePage, 'Kirby\Cms\Page') === true) {
|
||||
return $this->homePage;
|
||||
}
|
||||
|
||||
if ($home = $this->find($this->homePageId())) {
|
||||
return $this->homePage = $home;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the global home page id
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function homePageId(): string
|
||||
{
|
||||
return $this->homePageId ?? 'home';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an inventory of all files
|
||||
* and children in the site directory
|
||||
*
|
||||
* @internal
|
||||
* @return array
|
||||
*/
|
||||
public function inventory(): array
|
||||
{
|
||||
if ($this->inventory !== null) {
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
$kirby = $this->kirby();
|
||||
|
||||
return $this->inventory = Dir::inventory(
|
||||
$this->root(),
|
||||
$kirby->contentExtension(),
|
||||
$kirby->contentIgnore(),
|
||||
$kirby->multilang()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the current object with the given site object
|
||||
*
|
||||
* @param mixed $site
|
||||
* @return bool
|
||||
*/
|
||||
public function is($site): bool
|
||||
{
|
||||
if (is_a($site, 'Kirby\Cms\Site') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this === $site;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root to the media folder for the site
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return $this->kirby()->root('media') . '/site';
|
||||
}
|
||||
|
||||
/**
|
||||
* The site's base url for any files
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
return $this->kirby()->url('media') . '/site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last modification date of all pages
|
||||
* in the content folder.
|
||||
*
|
||||
* @param string|null $format
|
||||
* @param string|null $handler
|
||||
* @return mixed
|
||||
*/
|
||||
public function modified(string $format = null, string $handler = null)
|
||||
{
|
||||
return Dir::modified($this->root(), $format, $handler ?? $this->kirby()->option('date.handler', 'date'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current page if `$path`
|
||||
* is not specified. Otherwise it will try
|
||||
* to find a page by the given path.
|
||||
*
|
||||
* If no current page is set with the page
|
||||
* prop, the home page will be returned if
|
||||
* it can be found. (see `Site::homePage()`)
|
||||
*
|
||||
* @param string $path
|
||||
* @return \Kirby\Cms\Page|null
|
||||
*/
|
||||
public function page(string $path = null)
|
||||
{
|
||||
if ($path !== null) {
|
||||
return $this->find($path);
|
||||
}
|
||||
|
||||
if (is_a($this->page, 'Kirby\Cms\Page') === true) {
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->page = $this->homePage();
|
||||
} catch (LogicException $e) {
|
||||
return $this->page = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for `Site::children()`
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function pages()
|
||||
{
|
||||
return $this->children();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
return 'site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
*/
|
||||
public function panelUrl(bool $relative = false): string
|
||||
{
|
||||
if ($relative === true) {
|
||||
return '/' . $this->panelPath();
|
||||
} else {
|
||||
return $this->kirby()->url('panel') . '/' . $this->panelPath();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the permissions object for this site
|
||||
*
|
||||
* @return \Kirby\Cms\SitePermissions
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return new SitePermissions($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Url
|
||||
*
|
||||
* @internal
|
||||
* @return string|null
|
||||
*/
|
||||
public function previewUrl(): ?string
|
||||
{
|
||||
$preview = $this->blueprint()->preview();
|
||||
|
||||
if ($preview === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($preview === true) {
|
||||
$url = $this->url();
|
||||
} else {
|
||||
$url = $preview;
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the content directory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
return $this->root = $this->root ?? $this->kirby()->root('content');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SiteRules class instance
|
||||
* which is being used in various methods
|
||||
* to check for valid actions and input.
|
||||
*
|
||||
* @return \Kirby\Cms\SiteRules
|
||||
*/
|
||||
protected function rules()
|
||||
{
|
||||
return new SiteRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all pages in the site
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $params
|
||||
* @return \Kirby\Cms\Pages
|
||||
*/
|
||||
public function search(string $query = null, $params = [])
|
||||
{
|
||||
return $this->index()->search($query, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Blueprint object
|
||||
*
|
||||
* @param array|null $blueprint
|
||||
* @return self
|
||||
*/
|
||||
protected function setBlueprint(array $blueprint = null)
|
||||
{
|
||||
if ($blueprint !== null) {
|
||||
$blueprint['model'] = $this;
|
||||
$this->blueprint = new SiteBlueprint($blueprint);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id of the error page, which
|
||||
* is used in the errorPage method
|
||||
* to get the default error page if nothing
|
||||
* else is set.
|
||||
*
|
||||
* @param string $id
|
||||
* @return self
|
||||
*/
|
||||
protected function setErrorPageId(string $id = 'error')
|
||||
{
|
||||
$this->errorPageId = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id of the home page, which
|
||||
* is used in the homePage method
|
||||
* to get the default home page if nothing
|
||||
* else is set.
|
||||
*
|
||||
* @param string $id
|
||||
* @return self
|
||||
*/
|
||||
protected function setHomePageId(string $id = 'home')
|
||||
{
|
||||
$this->homePageId = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current page object
|
||||
*
|
||||
* @internal
|
||||
* @param \Kirby\Cms\Page|null $page
|
||||
* @return self
|
||||
*/
|
||||
public function setPage(Page $page = null)
|
||||
{
|
||||
$this->page = $page;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Url
|
||||
*
|
||||
* @param string $url
|
||||
* @return self
|
||||
*/
|
||||
protected function setUrl($url = null)
|
||||
{
|
||||
$this->url = $url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the most important site
|
||||
* properties to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'children' => $this->children()->keys(),
|
||||
'content' => $this->content()->toArray(),
|
||||
'errorPage' => $this->errorPage() ? $this->errorPage()->id(): false,
|
||||
'files' => $this->files()->keys(),
|
||||
'homePage' => $this->homePage() ? $this->homePage()->id(): false,
|
||||
'page' => $this->page() ? $this->page()->id(): false,
|
||||
'title' => $this->title()->value(),
|
||||
'url' => $this->url(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Url
|
||||
*
|
||||
* @param string|null $language
|
||||
* @return string
|
||||
*/
|
||||
public function url($language = null): string
|
||||
{
|
||||
if ($language !== null || $this->kirby()->multilang() === true) {
|
||||
return $this->urlForLanguage($language);
|
||||
}
|
||||
|
||||
return $this->url ?? $this->kirby()->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translated url
|
||||
*
|
||||
* @internal
|
||||
* @param string $languageCode
|
||||
* @param array $options
|
||||
* @return string
|
||||
*/
|
||||
public function urlForLanguage(string $languageCode = null, array $options = null): string
|
||||
{
|
||||
if ($language = $this->kirby()->language($languageCode)) {
|
||||
return $language->url();
|
||||
}
|
||||
|
||||
return $this->kirby()->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current page by
|
||||
* id or page object and
|
||||
* returns the current page
|
||||
*
|
||||
* @internal
|
||||
* @param string|\Kirby\Cms\Page $page
|
||||
* @param string|null $languageCode
|
||||
* @return \Kirby\Cms\Page
|
||||
*/
|
||||
public function visit($page, string $languageCode = null)
|
||||
{
|
||||
if ($languageCode !== null) {
|
||||
$this->kirby()->setCurrentTranslation($languageCode);
|
||||
$this->kirby()->setCurrentLanguage($languageCode);
|
||||
}
|
||||
|
||||
// convert ids to a Page object
|
||||
if (is_string($page)) {
|
||||
$page = $this->find($page);
|
||||
}
|
||||
|
||||
// handle invalid pages
|
||||
if (is_a($page, 'Kirby\Cms\Page') === false) {
|
||||
throw new InvalidArgumentException('Invalid page object');
|
||||
}
|
||||
|
||||
// set the current active page
|
||||
$this->setPage($page);
|
||||
|
||||
// return the page
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any content of the site has been
|
||||
* modified after the given unix timestamp
|
||||
* This is mainly used to auto-update the cache
|
||||
*
|
||||
* @param mixed $time
|
||||
* @return bool
|
||||
*/
|
||||
public function wasModifiedAfter($time): bool
|
||||
{
|
||||
return Dir::wasModifiedAfter($this->root(), $time);
|
||||
}
|
||||
}
|
||||
98
kirby/src/Cms/SiteActions.php
Normal file
98
kirby/src/Cms/SiteActions.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* SiteActions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait SiteActions
|
||||
{
|
||||
/**
|
||||
* Commits a site action, by following these steps
|
||||
*
|
||||
* 1. checks the action rules
|
||||
* 2. sends the before hook
|
||||
* 3. commits the store action
|
||||
* 4. sends the after hook
|
||||
* 5. returns the result
|
||||
*
|
||||
* @param string $action
|
||||
* @param mixed ...$arguments
|
||||
* @param Closure $callback
|
||||
* @return mixed
|
||||
*/
|
||||
protected function commit(string $action, array $arguments, Closure $callback)
|
||||
{
|
||||
$old = $this->hardcopy();
|
||||
$kirby = $this->kirby();
|
||||
$argumentValues = array_values($arguments);
|
||||
|
||||
$this->rules()->$action(...$argumentValues);
|
||||
$kirby->trigger('site.' . $action . ':before', $arguments);
|
||||
|
||||
$result = $callback(...$argumentValues);
|
||||
|
||||
$kirby->trigger('site.' . $action . ':after', ['newSite' => $result, 'oldSite' => $old]);
|
||||
|
||||
$kirby->cache('pages')->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the site title
|
||||
*
|
||||
* @param string $title
|
||||
* @param string|null $languageCode
|
||||
* @return self
|
||||
*/
|
||||
public function changeTitle(string $title, string $languageCode = null)
|
||||
{
|
||||
$arguments = ['site' => $this, 'title' => $title, 'languageCode' => $languageCode];
|
||||
return $this->commit('changeTitle', $arguments, function ($site, $title, $languageCode) {
|
||||
return $site->save(['title' => $title], $languageCode);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a main page
|
||||
*
|
||||
* @param array $props
|
||||
* @return \Kirby\Cms\Page
|
||||
*/
|
||||
public function createChild(array $props)
|
||||
{
|
||||
$props = array_merge($props, [
|
||||
'url' => null,
|
||||
'num' => null,
|
||||
'parent' => null,
|
||||
'site' => $this,
|
||||
]);
|
||||
|
||||
return Page::create($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean internal caches
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function purge()
|
||||
{
|
||||
$this->blueprint = null;
|
||||
$this->children = null;
|
||||
$this->content = null;
|
||||
$this->files = null;
|
||||
$this->inventory = null;
|
||||
$this->translations = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
60
kirby/src/Cms/SiteBlueprint.php
Normal file
60
kirby/src/Cms/SiteBlueprint.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of the basic blueprint class
|
||||
* to handle the blueprint for the site.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class SiteBlueprint extends Blueprint
|
||||
{
|
||||
/**
|
||||
* Creates a new page blueprint object
|
||||
* with the given props
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
parent::__construct($props);
|
||||
|
||||
// normalize all available page options
|
||||
$this->props['options'] = $this->normalizeOptions(
|
||||
$props['options'] ?? true,
|
||||
// defaults
|
||||
[
|
||||
'changeTitle' => null,
|
||||
'update' => null,
|
||||
],
|
||||
// aliases
|
||||
[
|
||||
'title' => 'changeTitle',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the preview settings
|
||||
* The preview setting controlls the "Open"
|
||||
* button in the panel and redirects it to a
|
||||
* different URL if necessary.
|
||||
*
|
||||
* @return string|bool
|
||||
*/
|
||||
public function preview()
|
||||
{
|
||||
$preview = $this->props['options']['preview'] ?? true;
|
||||
|
||||
if (is_string($preview) === true) {
|
||||
return $this->model->toString($preview);
|
||||
}
|
||||
|
||||
return $preview;
|
||||
}
|
||||
}
|
||||
17
kirby/src/Cms/SitePermissions.php
Normal file
17
kirby/src/Cms/SitePermissions.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* SitePermissions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class SitePermissions extends ModelPermissions
|
||||
{
|
||||
protected $category = 'site';
|
||||
}
|
||||
41
kirby/src/Cms/SiteRules.php
Normal file
41
kirby/src/Cms/SiteRules.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Validators for all site actions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class SiteRules
|
||||
{
|
||||
public static function changeTitle(Site $site, string $title): bool
|
||||
{
|
||||
if ($site->permissions()->changeTitle() !== true) {
|
||||
throw new PermissionException(['key' => 'site.changeTitle.permission']);
|
||||
}
|
||||
|
||||
if (Str::length($title) === 0) {
|
||||
throw new InvalidArgumentException(['key' => 'site.changeTitle.empty']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function update(Site $site, array $content = []): bool
|
||||
{
|
||||
if ($site->permissions()->update() !== true) {
|
||||
throw new PermissionException(['key' => 'site.update.permission']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
63
kirby/src/Cms/Structure.php
Normal file
63
kirby/src/Cms/Structure.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* The Structure class wraps
|
||||
* array data into a nicely chainable
|
||||
* collection with objects and Kirby-style
|
||||
* content with fields. The Structure class
|
||||
* is the heart and soul of our yaml conversion
|
||||
* method for pages.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Structure extends Collection
|
||||
{
|
||||
/**
|
||||
* Creates a new Collection with the given objects
|
||||
*
|
||||
* @param array $objects
|
||||
* @param object $parent
|
||||
*/
|
||||
public function __construct($objects = [], $parent = null)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$this->set($objects);
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal setter for collection items.
|
||||
* This makes sure that nothing unexpected ends
|
||||
* up in the collection. You can pass arrays or
|
||||
* StructureObjects
|
||||
*
|
||||
* @param string $id
|
||||
* @param array|StructureObject $props
|
||||
*/
|
||||
public function __set(string $id, $props)
|
||||
{
|
||||
if (is_a($props, 'Kirby\Cms\StructureObject') === true) {
|
||||
$object = $props;
|
||||
} else {
|
||||
if (is_array($props) === false) {
|
||||
throw new InvalidArgumentException('Invalid structure data');
|
||||
}
|
||||
|
||||
$object = new StructureObject([
|
||||
'content' => $props,
|
||||
'id' => $props['id'] ?? $id,
|
||||
'parent' => $this->parent,
|
||||
'structure' => $this
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::__set($object->id(), $object);
|
||||
}
|
||||
}
|
||||
210
kirby/src/Cms/StructureObject.php
Normal file
210
kirby/src/Cms/StructureObject.php
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The StructureObject reprents each item
|
||||
* in a Structure collection. StructureObjects
|
||||
* behave pretty much the same as Pages or Users
|
||||
* and have a Content object to access their fields.
|
||||
* All fields in a StructureObject are therefor also
|
||||
* wrapped in a Field object and can be accessed in
|
||||
* the same way as Page fields. They also use the same
|
||||
* Field methods.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class StructureObject extends Model
|
||||
{
|
||||
use HasSiblings;
|
||||
|
||||
/**
|
||||
* The content
|
||||
*
|
||||
* @var Content
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var Page|Site|File|User
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* The parent Structure collection
|
||||
*
|
||||
* @var Structure
|
||||
*/
|
||||
protected $structure;
|
||||
|
||||
/**
|
||||
* Modified getter to also return fields
|
||||
* from the object's content
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
// public property access
|
||||
if (isset($this->$method) === true) {
|
||||
return $this->$method;
|
||||
}
|
||||
|
||||
return $this->content()->get($method, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new StructureObject with the given props
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content
|
||||
*
|
||||
* @return \Kirby\Cms\Content
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
if (is_a($this->content, 'Kirby\Cms\Content') === true) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
if (is_array($this->content) !== true) {
|
||||
$this->content = [];
|
||||
}
|
||||
|
||||
return $this->content = new Content($this->content, $this->parent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the required id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the current object with the given structure object
|
||||
*
|
||||
* @param mixed $structure
|
||||
* @return bool
|
||||
*/
|
||||
public function is($structure): bool
|
||||
{
|
||||
if (is_a($structure, 'Kirby\Cms\StructureObject') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this === $structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Model object
|
||||
*
|
||||
* @return \Kirby\Cms\Model
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content object with the given parent
|
||||
*
|
||||
* @param array|null $content
|
||||
* @return self
|
||||
*/
|
||||
protected function setContent(array $content = null)
|
||||
{
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the id of the object.
|
||||
* The id is required. The structure
|
||||
* class will use the index, if no id is
|
||||
* specified.
|
||||
*
|
||||
* @param string $id
|
||||
* @return self
|
||||
*/
|
||||
protected function setId(string $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent Model. This can either be a
|
||||
* Page, Site, File or User object
|
||||
*
|
||||
* @param \Kirby\Cms\Model|null $parent
|
||||
* @return self
|
||||
*/
|
||||
protected function setParent(Model $parent = null)
|
||||
{
|
||||
$this->parent = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent Structure collection
|
||||
*
|
||||
* @param \Kirby\Cms\Structure $structure
|
||||
* @return self
|
||||
*/
|
||||
protected function setStructure(Structure $structure = null)
|
||||
{
|
||||
$this->structure = $structure;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Structure collection as siblings
|
||||
*
|
||||
* @return \Kirby\Cms\Structure
|
||||
*/
|
||||
protected function siblingsCollection()
|
||||
{
|
||||
return $this->structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all fields in the object to a
|
||||
* plain associative array. The id is
|
||||
* injected into the array afterwards
|
||||
* to make sure it's always present and
|
||||
* not overloaded in the content.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = $this->content()->toArray();
|
||||
$array['id'] = $this->id();
|
||||
|
||||
ksort($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
492
kirby/src/Cms/System.php
Normal file
492
kirby/src/Cms/System.php
Normal file
|
|
@ -0,0 +1,492 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Json;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Http\Remote;
|
||||
use Kirby\Http\Uri;
|
||||
use Kirby\Http\Url;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The System class gathers all information
|
||||
* about the server, PHP and other environment
|
||||
* parameters and checks for a valid setup.
|
||||
*
|
||||
* This is mostly used by the panel installer
|
||||
* to check if the panel can be installed at all.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class System
|
||||
{
|
||||
/**
|
||||
* @var App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\App $app
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
|
||||
// try to create all folders that could be missing
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an status array of all checks
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function status(): array
|
||||
{
|
||||
return [
|
||||
'accounts' => $this->accounts(),
|
||||
'content' => $this->content(),
|
||||
'curl' => $this->curl(),
|
||||
'sessions' => $this->sessions(),
|
||||
'mbstring' => $this->mbstring(),
|
||||
'media' => $this->media(),
|
||||
'php' => $this->php(),
|
||||
'server' => $this->server(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a writable accounts folder
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function accounts(): bool
|
||||
{
|
||||
return is_writable($this->app->root('accounts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a writable content folder
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function content(): bool
|
||||
{
|
||||
return is_writable($this->app->root('content'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an existing curl extension
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function curl(): bool
|
||||
{
|
||||
return extension_loaded('curl');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app's human-readable
|
||||
* index URL without scheme
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function indexUrl(): string
|
||||
{
|
||||
$url = $this->app->url('index');
|
||||
|
||||
if (Url::isAbsolute($url)) {
|
||||
$uri = Url::toObject($url);
|
||||
} else {
|
||||
// index URL was configured without host, use the current host
|
||||
$uri = Uri::current([
|
||||
'path' => $url,
|
||||
'query' => null
|
||||
]);
|
||||
}
|
||||
|
||||
return $uri->setScheme(null)->setSlash(false)->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the most important folders
|
||||
* if they don't exist yet
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
// init /site/accounts
|
||||
try {
|
||||
Dir::make($this->app->root('accounts'));
|
||||
} catch (Throwable $e) {
|
||||
throw new PermissionException('The accounts directory could not be created');
|
||||
}
|
||||
|
||||
// init /content
|
||||
try {
|
||||
Dir::make($this->app->root('content'));
|
||||
} catch (Throwable $e) {
|
||||
throw new PermissionException('The content directory could not be created');
|
||||
}
|
||||
|
||||
// init /media
|
||||
try {
|
||||
Dir::make($this->app->root('media'));
|
||||
} catch (Throwable $e) {
|
||||
throw new PermissionException('The media directory could not be created');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the panel is installable.
|
||||
* On a public server the panel.install
|
||||
* option must be explicitly set to true
|
||||
* to get the installer up and running.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInstallable(): bool
|
||||
{
|
||||
return $this->isLocal() === true || $this->app->option('panel.install', false) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Kirby is already installed
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
return $this->app->users()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a local installation
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLocal(): bool
|
||||
{
|
||||
$server = $this->app->server();
|
||||
$visitor = $this->app->visitor();
|
||||
$host = $server->host();
|
||||
|
||||
if ($host === 'localhost') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Str::endsWith($host, '.local') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Str::endsWith($host, '.test') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($visitor->ip(), ['::1', '127.0.0.1']) === true) {
|
||||
// ensure that there is no reverse proxy in between
|
||||
|
||||
if (
|
||||
isset($_SERVER['HTTP_X_FORWARDED_FOR']) === true &&
|
||||
in_array($_SERVER['HTTP_X_FORWARDED_FOR'], ['::1', '127.0.0.1']) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($_SERVER['HTTP_CLIENT_IP']) === true &&
|
||||
in_array($_SERVER['HTTP_CLIENT_IP'], ['::1', '127.0.0.1']) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// no reverse proxy or the real client also comes from localhost
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all tests pass
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isOk(): bool
|
||||
{
|
||||
return in_array(false, array_values($this->status()), true) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the app's index URL for
|
||||
* licensing purposes
|
||||
*
|
||||
* @param string|null $url Input URL, by default the app's index URL
|
||||
* @return string Normalized URL
|
||||
*/
|
||||
protected function licenseUrl(string $url = null): string
|
||||
{
|
||||
if ($url === null) {
|
||||
$url = $this->indexUrl();
|
||||
}
|
||||
|
||||
// remove common "testing" subdomains as well as www.
|
||||
// to ensure that installations of the same site have
|
||||
// the same license URL; only for installations at /,
|
||||
// subdirectory installations are difficult to normalize
|
||||
if (Str::contains($url, '/') === false) {
|
||||
if (Str::startsWith($url, 'www.')) {
|
||||
return substr($url, 4);
|
||||
}
|
||||
|
||||
if (Str::startsWith($url, 'dev.')) {
|
||||
return substr($url, 4);
|
||||
}
|
||||
|
||||
if (Str::startsWith($url, 'test.')) {
|
||||
return substr($url, 5);
|
||||
}
|
||||
|
||||
if (Str::startsWith($url, 'staging.')) {
|
||||
return substr($url, 8);
|
||||
}
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the license file and returns
|
||||
* the license information if available
|
||||
*
|
||||
* @return string|bool License key or `false` if the current user has
|
||||
* permissions for access.settings, otherwise just a
|
||||
* boolean that tells whether a valid license is active
|
||||
*/
|
||||
public function license()
|
||||
{
|
||||
try {
|
||||
$license = Json::read($this->app->root('config') . '/.license');
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for all required fields for the validation
|
||||
if (isset(
|
||||
$license['license'],
|
||||
$license['order'],
|
||||
$license['date'],
|
||||
$license['email'],
|
||||
$license['domain'],
|
||||
$license['signature']
|
||||
) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// build the license verification data
|
||||
$data = [
|
||||
'license' => $license['license'],
|
||||
'order' => $license['order'],
|
||||
'email' => hash('sha256', $license['email'] . 'kwAHMLyLPBnHEskzH9pPbJsBxQhKXZnX'),
|
||||
'domain' => $license['domain'],
|
||||
'date' => $license['date']
|
||||
];
|
||||
|
||||
|
||||
// get the public key
|
||||
$pubKey = F::read($this->app->root('kirby') . '/kirby.pub');
|
||||
|
||||
// verify the license signature
|
||||
if (openssl_verify(json_encode($data), hex2bin($license['signature']), $pubKey, 'RSA-SHA256') !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// verify the URL
|
||||
if ($this->licenseUrl() !== $this->licenseUrl($license['domain'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// only return the actual license key if the
|
||||
// current user has appropriate permissions
|
||||
$user = $this->app->user();
|
||||
if ($user && $user->role()->permissions()->for('access', 'settings') === true) {
|
||||
return $license['license'];
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an existing mbstring extension
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function mbString(): bool
|
||||
{
|
||||
return extension_loaded('mbstring');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a writable media folder
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function media(): bool
|
||||
{
|
||||
return is_writable($this->app->root('media'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a valid PHP version
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function php(): bool
|
||||
{
|
||||
return version_compare(phpversion(), '7.1.0', '>=');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the license key
|
||||
* and adds it to the .license file in the config
|
||||
* folder if possible.
|
||||
*
|
||||
* @param string $license
|
||||
* @param string $email
|
||||
* @return bool
|
||||
*/
|
||||
public function register(string $license = null, string $email = null): bool
|
||||
{
|
||||
if (Str::startsWith($license, 'K3-PRO-') === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'license.format'
|
||||
]);
|
||||
}
|
||||
|
||||
if (V::email($email) === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'license.email'
|
||||
]);
|
||||
}
|
||||
|
||||
$response = Remote::get('https://licenses.getkirby.com/register', [
|
||||
'data' => [
|
||||
'license' => $license,
|
||||
'email' => $email,
|
||||
'domain' => $this->indexUrl()
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->code() !== 200) {
|
||||
throw new Exception($response->content());
|
||||
}
|
||||
|
||||
// decode the response
|
||||
$json = Json::decode($response->content());
|
||||
|
||||
// replace the email with the plaintext version
|
||||
$json['email'] = $email;
|
||||
|
||||
// where to store the license file
|
||||
$file = $this->app->root('config') . '/.license';
|
||||
|
||||
// save the license information
|
||||
Json::write($file, $json);
|
||||
|
||||
if ($this->license() === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'license.verification'
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a valid server environment
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function server(): bool
|
||||
{
|
||||
if ($servers = $this->app->option('servers')) {
|
||||
$servers = A::wrap($servers);
|
||||
} else {
|
||||
$servers = [
|
||||
'apache',
|
||||
'caddy',
|
||||
'litespeed',
|
||||
'nginx',
|
||||
'php'
|
||||
];
|
||||
}
|
||||
|
||||
$software = $_SERVER['SERVER_SOFTWARE'] ?? null;
|
||||
|
||||
return preg_match('!(' . implode('|', $servers) . ')!i', $software) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a writable sessions folder
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function sessions(): bool
|
||||
{
|
||||
return is_writable($this->app->root('sessions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the status as array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->status();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade to the new folder separator
|
||||
*
|
||||
* @param string $root
|
||||
* @return void
|
||||
*/
|
||||
public static function upgradeContent(string $root)
|
||||
{
|
||||
$index = Dir::read($root);
|
||||
|
||||
foreach ($index as $dir) {
|
||||
$oldRoot = $root . '/' . $dir;
|
||||
$newRoot = preg_replace('!\/([0-9]+)\-!', '/$1_', $oldRoot);
|
||||
|
||||
if (is_dir($oldRoot) === true) {
|
||||
Dir::move($oldRoot, $newRoot);
|
||||
static::upgradeContent($newRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
201
kirby/src/Cms/Template.php
Normal file
201
kirby/src/Cms/Template.php
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Tpl;
|
||||
|
||||
/**
|
||||
* Represents a Kirby template and takes care
|
||||
* of loading the correct file.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Template
|
||||
{
|
||||
/**
|
||||
* Global template data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $data = [];
|
||||
|
||||
/**
|
||||
* The name of the template
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* Template type (html, json, etc.)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* Default template type if no specific type is set
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $defaultType;
|
||||
|
||||
/**
|
||||
* Creates a new template object
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $type
|
||||
* @param string $defaultType
|
||||
*/
|
||||
public function __construct(string $name, string $type = 'html', string $defaultType = 'html')
|
||||
{
|
||||
$this->name = strtolower($name);
|
||||
$this->type = $type;
|
||||
$this->defaultType = $defaultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object to a simple string
|
||||
* This is used in template filters for example
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the template exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->file());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected template file extension
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function extension(): string
|
||||
{
|
||||
return 'php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default template type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function defaultType(): string
|
||||
{
|
||||
return $this->defaultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the place where templates are located
|
||||
* in the site folder and and can be found in extensions
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function store(): string
|
||||
{
|
||||
return 'templates';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the location of the template file
|
||||
* if it exists.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function file(): ?string
|
||||
{
|
||||
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) {
|
||||
// ignore errors, continue searching
|
||||
}
|
||||
|
||||
// Look for the default template provided by an extension.
|
||||
$path = App::instance()->extension($this->store(), $this->name());
|
||||
|
||||
if ($path !== null) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
$name = $this->name() . '.' . $this->type();
|
||||
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the template name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public function render(array $data = []): string
|
||||
{
|
||||
return Tpl::load($this->file(), $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root to the templates directory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
return App::instance()->root($this->store());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the template type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the template uses the default type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDefaultType(): bool
|
||||
{
|
||||
$type = $this->type();
|
||||
|
||||
return $type === null || $type === $this->defaultType();
|
||||
}
|
||||
}
|
||||
193
kirby/src/Cms/Translation.php
Normal file
193
kirby/src/Cms/Translation.php
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Wrapper around Kirby's localization files,
|
||||
* which are store in `kirby/translations`.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Translation
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(string $code, array $data)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation author
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function author(): string
|
||||
{
|
||||
return $this->get('translation.author', 'Kirby');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the official translation code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function code(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all
|
||||
* translation strings
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation data and merges
|
||||
* it with the data from the default translation
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function dataWithFallback(): array
|
||||
{
|
||||
if ($this->code === 'en') {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
// get the fallback array
|
||||
$fallback = App::instance()->translation('en')->data();
|
||||
|
||||
return array_merge($fallback, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the writing direction
|
||||
* (ltr or rtl)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function direction(): string
|
||||
{
|
||||
return $this->get('translation.direction', 'ltr');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single translation
|
||||
* string by key
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $default
|
||||
* @return void
|
||||
*/
|
||||
public function get(string $key, string $default = null): ?string
|
||||
{
|
||||
return $this->data[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation id,
|
||||
* which is also the code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the translation from the
|
||||
* json file in Kirby's translations folder
|
||||
*
|
||||
* @param string $code
|
||||
* @param string $root
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function load(string $code, string $root, array $inject = [])
|
||||
{
|
||||
try {
|
||||
return new Translation($code, array_merge(Data::read($root), $inject));
|
||||
} catch (Exception $e) {
|
||||
return new Translation($code, []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PHP locale of the translation
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function locale(): string
|
||||
{
|
||||
$default = $this->code;
|
||||
if (Str::contains($default, '_') !== true) {
|
||||
$default .= '_' . strtoupper($this->code);
|
||||
}
|
||||
|
||||
return $this->get('translation.locale', $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable translation name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->get('translation.name', $this->code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the most important
|
||||
* properties to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->code(),
|
||||
'data' => $this->data(),
|
||||
'name' => $this->name(),
|
||||
'author' => $this->author(),
|
||||
];
|
||||
}
|
||||
}
|
||||
70
kirby/src/Cms/Translations.php
Normal file
70
kirby/src/Cms/Translations.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* A collection of all available Translations.
|
||||
* Provides a factory method to convert an array
|
||||
* to a collection of Translation objects and load
|
||||
* method to load all translations from disk
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Translations extends Collection
|
||||
{
|
||||
public function start(string $code): void
|
||||
{
|
||||
F::move($this->parent->contentFile('', true), $this->parent->contentFile($code, true));
|
||||
}
|
||||
|
||||
public function stop(string $code): void
|
||||
{
|
||||
F::move($this->parent->contentFile($code, true), $this->parent->contentFile('', true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $translations
|
||||
* @return self
|
||||
*/
|
||||
public static function factory(array $translations)
|
||||
{
|
||||
$collection = new static();
|
||||
|
||||
foreach ($translations as $code => $props) {
|
||||
$translation = new Translation($code, $props);
|
||||
$collection->data[$translation->code()] = $translation;
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $root
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function load(string $root, array $inject = [])
|
||||
{
|
||||
$collection = new static();
|
||||
|
||||
foreach (Dir::read($root) as $filename) {
|
||||
if (F::extension($filename) !== 'json') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$locale = F::name($filename);
|
||||
$translation = Translation::load($locale, $root . '/' . $filename, $inject[$locale] ?? []);
|
||||
|
||||
$collection->data[$locale] = $translation;
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
}
|
||||
69
kirby/src/Cms/Url.php
Normal file
69
kirby/src/Cms/Url.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Http\Url as BaseUrl;
|
||||
|
||||
/**
|
||||
* The `Url` class extends the
|
||||
* `Kirby\Http\Url` class. In addition
|
||||
* to the methods of that class for dealing
|
||||
* with URLs, it provides a specific
|
||||
* `Url::home` method that always creates
|
||||
* the correct base URL and a template asset
|
||||
* URL builder.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Url extends BaseUrl
|
||||
{
|
||||
public static $home = null;
|
||||
|
||||
/**
|
||||
* Returns the Url to the homepage
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function home(): string
|
||||
{
|
||||
return App::instance()->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function toTemplateAsset(string $assetPath, string $extension)
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$page = $kirby->site()->page();
|
||||
$path = $assetPath . '/' . $page->template() . '.' . $extension;
|
||||
$file = $kirby->root('assets') . '/' . $path;
|
||||
$url = $kirby->url('assets') . '/' . $path;
|
||||
|
||||
return file_exists($file) === true ? $url : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart resolver for internal and external urls
|
||||
*
|
||||
* @param string $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
|
||||
{
|
||||
$kirby = App::instance();
|
||||
|
||||
return $kirby->component('url')($kirby, $path, $options, function (string $path = null, $options = null) use ($kirby) {
|
||||
return $kirby->nativeComponent('url')($kirby, $path, $options);
|
||||
});
|
||||
}
|
||||
}
|
||||
934
kirby/src/Cms/User.php
Normal file
934
kirby/src/Cms/User.php
Normal file
|
|
@ -0,0 +1,934 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Session\Session;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The `$user` object represents a
|
||||
* single Panel or frontend user.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class User extends ModelWithContent
|
||||
{
|
||||
const CLASS_ALIAS = 'user';
|
||||
|
||||
use HasFiles;
|
||||
use HasMethods;
|
||||
use HasSiblings;
|
||||
use UserActions;
|
||||
|
||||
/**
|
||||
* @var File
|
||||
*/
|
||||
protected $avatar;
|
||||
|
||||
/**
|
||||
* @var UserBlueprint
|
||||
*/
|
||||
protected $blueprint;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $credentials;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $email;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $hash;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
protected $inventory;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $language;
|
||||
|
||||
/**
|
||||
* All registered user methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $methods = [];
|
||||
|
||||
/**
|
||||
* Registry with all User models
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $models = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $password;
|
||||
|
||||
/**
|
||||
* The user role
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* Modified getter to also return fields
|
||||
* from the content
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
// public property access
|
||||
if (isset($this->$method) === true) {
|
||||
return $this->$method;
|
||||
}
|
||||
|
||||
// user methods
|
||||
if ($this->hasMethod($method)) {
|
||||
return $this->callMethod($method, $arguments);
|
||||
}
|
||||
|
||||
// return site content otherwise
|
||||
return $this->content()->get($method, $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new User object
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$props['id'] = $props['id'] ?? $this->createId();
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return array_merge($this->toArray(), [
|
||||
'avatar' => $this->avatar(),
|
||||
'content' => $this->content(),
|
||||
'role' => $this->role()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the api endpoint
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
*/
|
||||
public function apiUrl(bool $relative = false): string
|
||||
{
|
||||
if ($relative === true) {
|
||||
return 'users/' . $this->id();
|
||||
} else {
|
||||
return $this->kirby()->url('api') . '/users/' . $this->id();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the File object for the avatar or null
|
||||
*
|
||||
* @return \Kirby\Cms\File|null
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
return $this->files()->template('avatar')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UserBlueprint object
|
||||
*
|
||||
* @return \Kirby\Cms\UserBlueprint
|
||||
*/
|
||||
public function blueprint()
|
||||
{
|
||||
if (is_a($this->blueprint, 'Kirby\Cms\Blueprint') === true) {
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->blueprint = UserBlueprint::factory('users/' . $this->role(), 'users/default', $this);
|
||||
} catch (Exception $e) {
|
||||
return $this->blueprint = new UserBlueprint([
|
||||
'model' => $this,
|
||||
'name' => 'default',
|
||||
'title' => 'Default',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the content for the write method
|
||||
*
|
||||
* @internal
|
||||
* @param array $data
|
||||
* @param string $languageCode Not used so far
|
||||
* @return array
|
||||
*/
|
||||
public function contentFileData(array $data, string $languageCode = null): array
|
||||
{
|
||||
// remove stuff that has nothing to do in the text files
|
||||
unset(
|
||||
$data['email'],
|
||||
$data['language'],
|
||||
$data['name'],
|
||||
$data['password'],
|
||||
$data['role']
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename for the content file
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileName(): string
|
||||
{
|
||||
return 'user';
|
||||
}
|
||||
|
||||
protected function credentials(): array
|
||||
{
|
||||
return $this->credentials = $this->credentials ?? $this->readCredentials();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user email address
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function email(): ?string
|
||||
{
|
||||
return $this->email = $this->email ?? $this->credentials()['email'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return is_file($this->contentFile('default')) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a User object and also
|
||||
* takes User models into account.
|
||||
*
|
||||
* @internal
|
||||
* @param mixed $props
|
||||
* @return self
|
||||
*/
|
||||
public static function factory($props)
|
||||
{
|
||||
if (empty($props['model']) === false) {
|
||||
return static::model($props['model'], $props);
|
||||
}
|
||||
|
||||
return new static($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes the user's password unless it is `null`,
|
||||
* which will leave it as `null`
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $password
|
||||
* @return string|null
|
||||
*/
|
||||
public static function hashPassword($password): ?string
|
||||
{
|
||||
if ($password !== null) {
|
||||
$password = password_hash($password, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the inventory of files
|
||||
* children and content files
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function inventory(): array
|
||||
{
|
||||
if ($this->inventory !== null) {
|
||||
return $this->inventory;
|
||||
}
|
||||
|
||||
$kirby = $this->kirby();
|
||||
|
||||
return $this->inventory = Dir::inventory(
|
||||
$this->root(),
|
||||
$kirby->contentExtension(),
|
||||
$kirby->contentIgnore(),
|
||||
$kirby->multilang()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the current object with the given user object
|
||||
*
|
||||
* @param \Kirby\Cms\User|null $user
|
||||
* @return bool
|
||||
*/
|
||||
public function is(User $user = null): bool
|
||||
{
|
||||
if ($user === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->id() === $user->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this user has the admin role
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role()->id() === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user is the virtual
|
||||
* Kirby user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isKirby(): bool
|
||||
{
|
||||
return $this->email() === 'kirby@getkirby.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user is this user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLoggedIn(): bool
|
||||
{
|
||||
return $this->is($this->kirby()->user());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is the last one
|
||||
* with the admin role
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLastAdmin(): bool
|
||||
{
|
||||
return $this->role()->isAdmin() === true && $this->kirby()->users()->filterBy('role', 'admin')->count() <= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is the last user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLastUser(): bool
|
||||
{
|
||||
return $this->kirby()->users()->count() === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function language(): string
|
||||
{
|
||||
return $this->language ?? $this->language = $this->credentials()['language'] ?? $this->kirby()->option('panel.language', 'en');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user in
|
||||
*
|
||||
* @param string $password
|
||||
* @param \Kirby\Session\Session|array $session Session options or session object to set the user in
|
||||
* @return bool
|
||||
*
|
||||
* @throws \Kirby\Exception\PermissionException If the password is not valid
|
||||
*/
|
||||
public function login(string $password, $session = null): bool
|
||||
{
|
||||
$this->validatePassword($password);
|
||||
$this->loginPasswordless($session);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user in without checking the password
|
||||
*
|
||||
* @param \Kirby\Session\Session|array $session Session options or session object to set the user in
|
||||
* @return void
|
||||
*/
|
||||
public function loginPasswordless($session = null): void
|
||||
{
|
||||
$kirby = $this->kirby();
|
||||
|
||||
$session = $this->sessionFromOptions($session);
|
||||
|
||||
$kirby->trigger('user.login:before', ['user' => $this, 'session' => $session]);
|
||||
|
||||
$session->regenerateToken(); // privilege change
|
||||
$session->data()->set('user.id', $this->id());
|
||||
$this->kirby()->auth()->setUser($this);
|
||||
|
||||
$kirby->trigger('user.login:after', ['user' => $this, 'session' => $session]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user out
|
||||
*
|
||||
* @param \Kirby\Session\Session|array $session Session options or session object to unset the user in
|
||||
* @return void
|
||||
*/
|
||||
public function logout($session = null): void
|
||||
{
|
||||
$kirby = $this->kirby();
|
||||
$session = $this->sessionFromOptions($session);
|
||||
|
||||
$kirby->trigger('user.logout:before', ['user' => $this, 'session' => $session]);
|
||||
|
||||
// remove the user from the session for future requests
|
||||
$session->data()->remove('user.id');
|
||||
|
||||
// clear the cached user object from the app state of the current request
|
||||
$this->kirby()->auth()->flush();
|
||||
|
||||
if ($session->data()->get() === []) {
|
||||
// session is now empty, we might as well destroy it
|
||||
$session->destroy();
|
||||
|
||||
$kirby->trigger('user.logout:after', ['user' => $this, 'session' => null]);
|
||||
} else {
|
||||
// privilege change
|
||||
$session->regenerateToken();
|
||||
|
||||
$kirby->trigger('user.logout:after', ['user' => $this, 'session' => $session]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root to the media folder for the user
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return $this->kirby()->root('media') . '/users/' . $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the media url for the user object
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
return $this->kirby()->url('media') . '/users/' . $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a user model if it has been registered
|
||||
*
|
||||
* @internal
|
||||
* @param string $name
|
||||
* @param array $props
|
||||
* @return \Kirby\Cms\User
|
||||
*/
|
||||
public static function model(string $name, array $props = [])
|
||||
{
|
||||
if ($class = (static::$models[$name] ?? null)) {
|
||||
$object = new $class($props);
|
||||
|
||||
if (is_a($object, 'Kirby\Cms\User') === true) {
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
||||
return new static($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification date of the user
|
||||
*
|
||||
* @param string $format
|
||||
* @param string|null $handler
|
||||
* @param string|null $languageCode
|
||||
* @return int|string
|
||||
*/
|
||||
public function modified(string $format = 'U', string $handler = null, string $languageCode = null)
|
||||
{
|
||||
$modifiedContent = F::modified($this->contentFile($languageCode));
|
||||
$modifiedIndex = F::modified($this->root() . '/index.php');
|
||||
$modifiedTotal = max([$modifiedContent, $modifiedIndex]);
|
||||
$handler = $handler ?? $this->kirby()->option('date.handler', 'date');
|
||||
|
||||
return $handler($format, $modifiedTotal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's name
|
||||
*
|
||||
* @return \Kirby\Cms\Field
|
||||
*/
|
||||
public function name()
|
||||
{
|
||||
if (is_string($this->name) === true) {
|
||||
return new Field($this, 'name', $this->name);
|
||||
}
|
||||
|
||||
if ($this->name !== null) {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
return $this->name = new Field($this, 'name', $this->credentials()['name'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dummy nobody
|
||||
*
|
||||
* @internal
|
||||
* @return self
|
||||
*/
|
||||
public static function nobody()
|
||||
{
|
||||
return new static([
|
||||
'email' => 'nobody@getkirby.com',
|
||||
'role' => 'nobody'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Panel icon definition
|
||||
*
|
||||
* @internal
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function panelIcon(array $params = null): array
|
||||
{
|
||||
$params['type'] = 'user';
|
||||
|
||||
return parent::panelIcon($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image file object based on provided query
|
||||
*
|
||||
* @internal
|
||||
* @param string|null $query
|
||||
* @return \Kirby\Cms\File|\Kirby\Cms\Asset|null
|
||||
*/
|
||||
protected function panelImageSource(string $query = null)
|
||||
{
|
||||
if ($query === null) {
|
||||
return $this->avatar();
|
||||
}
|
||||
|
||||
return parent::panelImageSource($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @internal
|
||||
* @return string
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
return 'users/' . $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns prepared data for the panel user picker
|
||||
*
|
||||
* @param array|null $params
|
||||
* @return array
|
||||
*/
|
||||
public function panelPickerData(array $params = null): array
|
||||
{
|
||||
$image = $this->panelImage($params['image'] ?? []);
|
||||
$icon = $this->panelIcon($image);
|
||||
|
||||
return [
|
||||
'icon' => $icon,
|
||||
'id' => $this->id(),
|
||||
'image' => $image,
|
||||
'email' => $this->email(),
|
||||
'info' => $this->toString($params['info'] ?? false),
|
||||
'link' => $this->panelUrl(true),
|
||||
'text' => $this->toString($params['text'] ?? '{{ user.username }}'),
|
||||
'username' => $this->username(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @internal
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
*/
|
||||
public function panelUrl(bool $relative = false): string
|
||||
{
|
||||
if ($relative === true) {
|
||||
return '/' . $this->panelPath();
|
||||
} else {
|
||||
return $this->kirby()->url('panel') . '/' . $this->panelPath();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encrypted user password
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function password(): ?string
|
||||
{
|
||||
if ($this->password !== null) {
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
return $this->password = $this->readPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Kirby\Cms\UserPermissions
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return new UserPermissions($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user role
|
||||
*
|
||||
* @return \Kirby\Cms\Role
|
||||
*/
|
||||
public function role()
|
||||
{
|
||||
if (is_a($this->role, 'Kirby\Cms\Role') === true) {
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
$roleName = $this->role ?? $this->credentials()['role'] ?? 'visitor';
|
||||
|
||||
if ($role = $this->kirby()->roles()->find($roleName)) {
|
||||
return $this->role = $role;
|
||||
}
|
||||
|
||||
return $this->role = Role::nobody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available roles
|
||||
* for this user, that can be selected
|
||||
* by the authenticated user
|
||||
*
|
||||
* @return \Kirby\Cms\Roles
|
||||
*/
|
||||
public function roles()
|
||||
{
|
||||
$kirby = $this->kirby();
|
||||
$roles = $kirby->roles();
|
||||
|
||||
// a collection with just the one role of the user
|
||||
$myRole = $roles->filterBy('id', $this->role()->id());
|
||||
|
||||
// if there's an authenticated user …
|
||||
if ($user = $kirby->user()) {
|
||||
|
||||
// admin users can select pretty much any role
|
||||
if ($user->isAdmin() === true) {
|
||||
// except if the user is the last admin
|
||||
if ($this->isLastAdmin() === true) {
|
||||
// in which case they have to stay admin
|
||||
return $myRole;
|
||||
}
|
||||
|
||||
// return all roles for mighty admins
|
||||
return $roles;
|
||||
}
|
||||
}
|
||||
|
||||
// any other user can only keep their role
|
||||
return $myRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* The absolute path to the user directory
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
return $this->kirby()->root('accounts') . '/' . $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UserRules class to
|
||||
* validate any important action.
|
||||
*
|
||||
* @return \Kirby\Cms\UserRules
|
||||
*/
|
||||
protected function rules()
|
||||
{
|
||||
return new UserRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Blueprint object
|
||||
*
|
||||
* @param array|null $blueprint
|
||||
* @return self
|
||||
*/
|
||||
protected function setBlueprint(array $blueprint = null)
|
||||
{
|
||||
if ($blueprint !== null) {
|
||||
$blueprint['model'] = $this;
|
||||
$this->blueprint = new UserBlueprint($blueprint);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user email
|
||||
*
|
||||
* @param string $email
|
||||
* @return self
|
||||
*/
|
||||
protected function setEmail(string $email = null)
|
||||
{
|
||||
if ($email !== null) {
|
||||
$this->email = strtolower(trim($email));
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user id
|
||||
*
|
||||
* @param string $id
|
||||
* @return self
|
||||
*/
|
||||
protected function setId(string $id = null)
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user language
|
||||
*
|
||||
* @param string $language
|
||||
* @return self
|
||||
*/
|
||||
protected function setLanguage(string $language = null)
|
||||
{
|
||||
$this->language = $language !== null ? trim($language) : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user name
|
||||
*
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
protected function setName(string $name = null)
|
||||
{
|
||||
$this->name = $name !== null ? trim($name) : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's password hash
|
||||
*
|
||||
* @param string $password
|
||||
* @return self
|
||||
*/
|
||||
protected function setPassword(string $password = null)
|
||||
{
|
||||
$this->password = $password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user role
|
||||
*
|
||||
* @param string $role
|
||||
* @return self
|
||||
*/
|
||||
protected function setRole(string $role = null)
|
||||
{
|
||||
$this->role = $role !== null ? strtolower(trim($role)) : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts session options into a session object
|
||||
*
|
||||
* @param \Kirby\Session\Session|array $session Session options or session object to unset the user in
|
||||
* @return \Kirby\Session\Session
|
||||
*/
|
||||
protected function sessionFromOptions($session)
|
||||
{
|
||||
// use passed session options or session object if set
|
||||
if (is_array($session) === true) {
|
||||
$session = $this->kirby()->session($session);
|
||||
} elseif (is_a($session, 'Kirby\Session\Session') === false) {
|
||||
$session = $this->kirby()->session(['detect' => true]);
|
||||
}
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Users collection
|
||||
*
|
||||
* @return \Kirby\Cms\Users
|
||||
*/
|
||||
protected function siblingsCollection()
|
||||
{
|
||||
return $this->kirby()->users();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the most important user properties
|
||||
* to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'avatar' => $this->avatar() ? $this->avatar()->toArray() : null,
|
||||
'content' => $this->content()->toArray(),
|
||||
'email' => $this->email(),
|
||||
'id' => $this->id(),
|
||||
'language' => $this->language(),
|
||||
'role' => $this->role()->name(),
|
||||
'username' => $this->username()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* String template builder
|
||||
*
|
||||
* @param string|null $template
|
||||
* @param array|null $data
|
||||
* @param string $fallback Fallback for tokens in the template that cannot be replaced
|
||||
* @return string
|
||||
*/
|
||||
public function toString(string $template = null, array $data = [], string $fallback = ''): string
|
||||
{
|
||||
if ($template === null) {
|
||||
$template = $this->email();
|
||||
}
|
||||
|
||||
return parent::toString($template, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username
|
||||
* which is the given name or the email
|
||||
* as a fallback
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function username(): ?string
|
||||
{
|
||||
return $this->name()->or($this->email())->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the given password with the stored one
|
||||
*
|
||||
* @param string $password
|
||||
* @return bool
|
||||
*
|
||||
* @throws \Kirby\Exception\NotFoundException If the user has no password
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the entered password does not match the user password
|
||||
*/
|
||||
public function validatePassword(string $password = null): bool
|
||||
{
|
||||
if (empty($this->password()) === true) {
|
||||
throw new NotFoundException(['key' => 'user.password.undefined']);
|
||||
}
|
||||
|
||||
if (Str::length($password) < 8) {
|
||||
throw new InvalidArgumentException(['key' => 'user.password.invalid']);
|
||||
}
|
||||
|
||||
if (password_verify($password, $this->password()) !== true) {
|
||||
throw new InvalidArgumentException(['key' => 'user.password.notSame']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
347
kirby/src/Cms/UserActions.php
Normal file
347
kirby/src/Cms/UserActions.php
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* UserActions
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
trait UserActions
|
||||
{
|
||||
/**
|
||||
* Changes the user email address
|
||||
*
|
||||
* @param string $email
|
||||
* @return self
|
||||
*/
|
||||
public function changeEmail(string $email)
|
||||
{
|
||||
return $this->commit('changeEmail', ['user' => $this, 'email' => $email], function ($user, $email) {
|
||||
$user = $user->clone([
|
||||
'email' => $email
|
||||
]);
|
||||
|
||||
$user->updateCredentials([
|
||||
'email' => $email
|
||||
]);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the user language
|
||||
*
|
||||
* @param string $language
|
||||
* @return self
|
||||
*/
|
||||
public function changeLanguage(string $language)
|
||||
{
|
||||
return $this->commit('changeLanguage', ['user' => $this, 'language' => $language], function ($user, $language) {
|
||||
$user = $user->clone([
|
||||
'language' => $language,
|
||||
]);
|
||||
|
||||
$user->updateCredentials([
|
||||
'language' => $language
|
||||
]);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the screen name of the user
|
||||
*
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
public function changeName(string $name)
|
||||
{
|
||||
return $this->commit('changeName', ['user' => $this, 'name' => $name], function ($user, $name) {
|
||||
$user = $user->clone([
|
||||
'name' => $name
|
||||
]);
|
||||
|
||||
$user->updateCredentials([
|
||||
'name' => $name
|
||||
]);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the user password
|
||||
*
|
||||
* @param string $password
|
||||
* @return self
|
||||
*/
|
||||
public function changePassword(string $password)
|
||||
{
|
||||
return $this->commit('changePassword', ['user' => $this, 'password' => $password], function ($user, $password) {
|
||||
$user = $user->clone([
|
||||
'password' => $password = User::hashPassword($password)
|
||||
]);
|
||||
|
||||
$user->writePassword($password);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the user role
|
||||
*
|
||||
* @param string $role
|
||||
* @return self
|
||||
*/
|
||||
public function changeRole(string $role)
|
||||
{
|
||||
return $this->commit('changeRole', ['user' => $this, 'role' => $role], function ($user, $role) {
|
||||
$user = $user->clone([
|
||||
'role' => $role,
|
||||
]);
|
||||
|
||||
$user->updateCredentials([
|
||||
'role' => $role
|
||||
]);
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits a user action, by following these steps
|
||||
*
|
||||
* 1. checks the action rules
|
||||
* 2. sends the before hook
|
||||
* 3. commits the action
|
||||
* 4. sends the after hook
|
||||
* 5. returns the result
|
||||
*
|
||||
* @param string $action
|
||||
* @param array $arguments
|
||||
* @param Closure $callback
|
||||
* @return mixed
|
||||
*/
|
||||
protected function commit(string $action, array $arguments = [], Closure $callback)
|
||||
{
|
||||
if ($this->isKirby() === true) {
|
||||
throw new PermissionException('The Kirby user cannot be changed');
|
||||
}
|
||||
|
||||
$old = $this->hardcopy();
|
||||
$kirby = $this->kirby();
|
||||
$argumentValues = array_values($arguments);
|
||||
|
||||
$this->rules()->$action(...$argumentValues);
|
||||
$kirby->trigger('user.' . $action . ':before', $arguments);
|
||||
|
||||
$result = $callback(...$argumentValues);
|
||||
|
||||
if ($action === 'create') {
|
||||
$argumentsAfter = ['user' => $result];
|
||||
} elseif ($action === 'delete') {
|
||||
$argumentsAfter = ['status' => $result, 'user' => $old];
|
||||
} else {
|
||||
$argumentsAfter = ['newUser' => $result, 'oldUser' => $old];
|
||||
}
|
||||
$kirby->trigger('user.' . $action . ':after', $argumentsAfter);
|
||||
|
||||
$kirby->cache('pages')->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new User from the given props and returns a new User object
|
||||
*
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public static function create(array $props = null)
|
||||
{
|
||||
$data = $props;
|
||||
|
||||
if (isset($props['password']) === true) {
|
||||
$data['password'] = User::hashPassword($props['password']);
|
||||
}
|
||||
|
||||
$props['role'] = $props['model'] = strtolower($props['role'] ?? 'default');
|
||||
|
||||
$user = User::factory($data);
|
||||
|
||||
// create a form for the user
|
||||
$form = Form::for($user, [
|
||||
'values' => $props['content'] ?? []
|
||||
]);
|
||||
|
||||
// inject the content
|
||||
$user = $user->clone(['content' => $form->strings(true)]);
|
||||
|
||||
// run the hook
|
||||
return $user->commit('create', ['user' => $user, 'input' => $props], function ($user, $props) {
|
||||
$user->writeCredentials([
|
||||
'email' => $user->email(),
|
||||
'language' => $user->language(),
|
||||
'name' => $user->name()->value(),
|
||||
'role' => $user->role()->id(),
|
||||
]);
|
||||
|
||||
$user->writePassword($user->password());
|
||||
|
||||
// always create users in the default language
|
||||
if ($user->kirby()->multilang() === true) {
|
||||
$languageCode = $user->kirby()->defaultLanguage()->code();
|
||||
} else {
|
||||
$languageCode = null;
|
||||
}
|
||||
|
||||
// add the user to users collection
|
||||
$user->kirby()->users()->add($user);
|
||||
|
||||
// write the user data
|
||||
return $user->save($user->content()->toArray(), $languageCode);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random user id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function createId(): string
|
||||
{
|
||||
$length = 8;
|
||||
$id = Str::random($length);
|
||||
|
||||
while ($this->kirby()->users()->has($id)) {
|
||||
$length++;
|
||||
$id = Str::random($length);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
return $this->commit('delete', ['user' => $this], function ($user) {
|
||||
if ($user->exists() === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// delete all public assets for this user
|
||||
Dir::remove($user->mediaRoot());
|
||||
|
||||
// delete the user directory
|
||||
if (Dir::remove($user->root()) !== true) {
|
||||
throw new LogicException('The user directory for "' . $user->email() . '" could not be deleted');
|
||||
}
|
||||
|
||||
// remove the user from users collection
|
||||
$user->kirby()->users()->remove($user);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the account information from disk
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function readCredentials(): array
|
||||
{
|
||||
$path = $this->root() . '/index.php';
|
||||
|
||||
if (is_file($path) === true) {
|
||||
$credentials = F::load($path);
|
||||
|
||||
return is_array($credentials) === false ? [] : $credentials;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the user password from disk
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function readPassword(): ?string
|
||||
{
|
||||
return F::read($this->root() . '/.htpasswd');
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the user data
|
||||
*
|
||||
* @param array $input
|
||||
* @param string $language
|
||||
* @param bool $validate
|
||||
* @return self
|
||||
*/
|
||||
public function update(array $input = null, string $language = null, bool $validate = false)
|
||||
{
|
||||
$user = parent::update($input, $language, $validate);
|
||||
|
||||
// set auth user data only if the current user is this user
|
||||
if ($user->isLoggedIn() === true) {
|
||||
$this->kirby()->auth()->setUser($user);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* This always merges the existing credentials
|
||||
* with the given input.
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
*/
|
||||
protected function updateCredentials(array $credentials): bool
|
||||
{
|
||||
return $this->writeCredentials(array_merge($this->credentials(), $credentials));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the account information to disk
|
||||
*
|
||||
* @param array $credentials
|
||||
* @return bool
|
||||
*/
|
||||
protected function writeCredentials(array $credentials): bool
|
||||
{
|
||||
return Data::write($this->root() . '/index.php', $credentials);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the password to disk
|
||||
*
|
||||
* @param string $password
|
||||
* @return bool
|
||||
*/
|
||||
protected function writePassword(string $password = null): bool
|
||||
{
|
||||
return F::write($this->root() . '/.htpasswd', $password);
|
||||
}
|
||||
}
|
||||
41
kirby/src/Cms/UserBlueprint.php
Normal file
41
kirby/src/Cms/UserBlueprint.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of the basic blueprint class
|
||||
* to handle all blueprints for users.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class UserBlueprint extends Blueprint
|
||||
{
|
||||
public function __construct(array $props)
|
||||
{
|
||||
// normalize and translate the description
|
||||
$props['description'] = $this->i18n($props['description'] ?? null);
|
||||
|
||||
// register the other props
|
||||
parent::__construct($props);
|
||||
|
||||
// normalize all available page options
|
||||
$this->props['options'] = $this->normalizeOptions(
|
||||
$props['options'] ?? true,
|
||||
// defaults
|
||||
[
|
||||
'create' => null,
|
||||
'changeEmail' => null,
|
||||
'changeLanguage' => null,
|
||||
'changeName' => null,
|
||||
'changePassword' => null,
|
||||
'changeRole' => null,
|
||||
'delete' => null,
|
||||
'update' => null,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
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