first version
This commit is contained in:
428
kirby/src/Api/Api.php
Executable file
428
kirby/src/Api/Api.php
Executable file
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Api;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Http\Router;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
class Api
|
||||
{
|
||||
use Properties;
|
||||
|
||||
protected $authentication;
|
||||
protected $debug = false;
|
||||
protected $collections = [];
|
||||
protected $data = [];
|
||||
protected $models = [];
|
||||
protected $routes = [];
|
||||
protected $requestData = [];
|
||||
protected $requestMethod;
|
||||
|
||||
public function __call($method, $args)
|
||||
{
|
||||
return $this->data($method, ...$args);
|
||||
}
|
||||
|
||||
public function __construct(array $props)
|
||||
{
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
public function authenticate()
|
||||
{
|
||||
if ($auth = $this->authentication()) {
|
||||
return $auth->call($this);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function authentication()
|
||||
{
|
||||
return $this->authentication;
|
||||
}
|
||||
|
||||
public function call(string $path = null, string $method = 'GET', array $requestData = [])
|
||||
{
|
||||
$path = rtrim($path, '/');
|
||||
|
||||
$this->setRequestMethod($method);
|
||||
$this->setRequestData($requestData);
|
||||
|
||||
$router = new Router($this->routes());
|
||||
$result = $router->find($path, $method);
|
||||
$auth = $result->attributes()['auth'] ?? true;
|
||||
|
||||
if ($auth !== false) {
|
||||
$this->authenticate();
|
||||
}
|
||||
|
||||
$output = $result->action()->call($this, ...$result->arguments());
|
||||
|
||||
if (is_object($output) === true) {
|
||||
return $this->resolve($output)->toResponse();
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
public function collections(): array
|
||||
{
|
||||
return $this->collections;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
public function hasData($key): bool
|
||||
{
|
||||
return isset($this->data[$key]) === true;
|
||||
}
|
||||
|
||||
public function model(string $name, $object = null)
|
||||
{
|
||||
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]);
|
||||
}
|
||||
|
||||
public function models(): array
|
||||
{
|
||||
return $this->models;
|
||||
}
|
||||
|
||||
public function requestData($type = null, $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;
|
||||
}
|
||||
|
||||
public function requestBody(string $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('body', $key, $default);
|
||||
}
|
||||
|
||||
public function requestFiles(string $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('files', $key, $default);
|
||||
}
|
||||
|
||||
public function requestHeaders(string $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('headers', $key, $default);
|
||||
}
|
||||
|
||||
public function requestMethod(): string
|
||||
{
|
||||
return $this->requestMethod;
|
||||
}
|
||||
|
||||
public function requestQuery(string $key = null, $default = null)
|
||||
{
|
||||
return $this->requestData('query', $key, $default);
|
||||
}
|
||||
|
||||
public function resolve($object)
|
||||
{
|
||||
if (is_a($object, 'Kirby\Api\Model') === true || is_a($object, 'Kirby\Api\Collection') === true) {
|
||||
return $object;
|
||||
}
|
||||
|
||||
$className = strtolower(get_class($object));
|
||||
$lastDash = strrpos($className, '\\');
|
||||
|
||||
if ($lastDash !== false) {
|
||||
$className = substr($className, $lastDash + 1);
|
||||
}
|
||||
|
||||
if (isset($this->models[$className]) === true) {
|
||||
return $this->model($className, $object);
|
||||
}
|
||||
|
||||
if (isset($this->collections[$className]) === true) {
|
||||
return $this->collection($className, $object);
|
||||
}
|
||||
|
||||
// now models deeply by checking for the actual type
|
||||
foreach ($this->models as $modelClass => $model) {
|
||||
if (is_a($object, $model['type']) === true) {
|
||||
return $this->model($modelClass, $object);
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundException(sprintf('The object "%s" cannot be resolved', $className));
|
||||
}
|
||||
|
||||
public function routes(): array
|
||||
{
|
||||
return $this->routes;
|
||||
}
|
||||
|
||||
protected function setAuthentication(Closure $authentication = null)
|
||||
{
|
||||
$this->authentication = $authentication;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setCollections(array $collections = null)
|
||||
{
|
||||
if ($collections !== null) {
|
||||
$this->collections = array_change_key_case($collections);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setData(array $data = null)
|
||||
{
|
||||
$this->data = $data ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setDebug(bool $debug = false)
|
||||
{
|
||||
$this->debug = $debug;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setModels(array $models = null)
|
||||
{
|
||||
if ($models !== null) {
|
||||
$this->models = array_change_key_case($models);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setRequestData(array $requestData = null)
|
||||
{
|
||||
$defaults = [
|
||||
'query' => [],
|
||||
'body' => [],
|
||||
'files' => []
|
||||
];
|
||||
|
||||
$this->requestData = array_merge($defaults, (array)$requestData);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setRequestMethod(string $requestMethod = null)
|
||||
{
|
||||
$this->requestMethod = $requestMethod;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setRoutes(array $routes = null)
|
||||
{
|
||||
$this->routes = $routes ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function render(string $path, $method = 'GET', array $requestData = [])
|
||||
{
|
||||
try {
|
||||
$result = $this->call($path, $method, $requestData);
|
||||
} catch (Throwable $e) {
|
||||
if (is_a($e, 'Kirby\Exception\Exception') === true) {
|
||||
$result = ['status' => 'error'] + $e->toArray();
|
||||
} else {
|
||||
$result = [
|
||||
'status' => 'error',
|
||||
'exception' => get_class($e),
|
||||
'message' => $e->getMessage(),
|
||||
'file' => ltrim($e->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? null),
|
||||
'line' => $e->getLine(),
|
||||
'code' => empty($e->getCode()) === false ? $e->getCode() : 500
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($result === null) {
|
||||
$result = [
|
||||
'status' => 'error',
|
||||
'message' => 'not found',
|
||||
'code' => 404,
|
||||
];
|
||||
}
|
||||
|
||||
if ($result === true) {
|
||||
$result = [
|
||||
'status' => 'ok',
|
||||
];
|
||||
}
|
||||
|
||||
if ($result === false) {
|
||||
$result = [
|
||||
'status' => 'error',
|
||||
'message' => 'bad request',
|
||||
'code' => 400,
|
||||
];
|
||||
}
|
||||
|
||||
if (is_array($result) === false) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// pretty print json data
|
||||
$pretty = (bool)($requestData['query']['pretty'] ?? false) === true;
|
||||
|
||||
// remove critical info from the result set if
|
||||
// debug mode is switched off
|
||||
if ($this->debug !== true) {
|
||||
unset(
|
||||
$result['file'],
|
||||
$result['exception'],
|
||||
$result['line']
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public function upload(Closure $callback, $single = false): array
|
||||
{
|
||||
$trials = 0;
|
||||
$uploads = [];
|
||||
$errors = [];
|
||||
$files = $this->requestFiles();
|
||||
|
||||
if (empty($files) === true) {
|
||||
throw new Exception('No uploaded files');
|
||||
}
|
||||
|
||||
foreach ($files as $upload) {
|
||||
if (isset($upload['tmp_name']) === false && is_array($upload)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$trials++;
|
||||
|
||||
try {
|
||||
if ($upload['error'] !== 0) {
|
||||
throw new Exception('Upload error');
|
||||
}
|
||||
|
||||
// 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('The uploaded file could not be moved');
|
||||
}
|
||||
|
||||
$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
|
||||
];
|
||||
}
|
||||
}
|
||||
124
kirby/src/Api/Collection.php
Executable file
124
kirby/src/Api/Collection.php
Executable file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Api;
|
||||
|
||||
use Closure;
|
||||
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.
|
||||
*/
|
||||
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'];
|
||||
$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;
|
||||
}
|
||||
}
|
||||
188
kirby/src/Api/Model.php
Executable file
188
kirby/src/Api/Model.php
Executable file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Api;
|
||||
|
||||
use Closure;
|
||||
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.
|
||||
*
|
||||
*/
|
||||
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]);
|
||||
}
|
||||
}
|
||||
77
kirby/src/Cache/ApcuCache.php
Executable file
77
kirby/src/Cache/ApcuCache.php
Executable file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
/**
|
||||
* APCu Cache Driver
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class ApcuCache extends Cache
|
||||
{
|
||||
|
||||
/**
|
||||
* Write an item to the cache for a given number of minutes.
|
||||
*
|
||||
* <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 void
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0)
|
||||
{
|
||||
return apcu_store($key, $this->value($value, $minutes)->toJson(), $this->expiration($minutes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an item from the cache.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
{
|
||||
return Value::fromJson(apcu_fetch($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current key exists in cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists(string $key): bool
|
||||
{
|
||||
return apcu_exists($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
return apcu_delete($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the entire cache directory
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
return apcu_clear_cache();
|
||||
}
|
||||
}
|
||||
237
kirby/src/Cache/Cache.php
Executable file
237
kirby/src/Cache/Cache.php
Executable file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
/**
|
||||
* Cache foundation
|
||||
* This class doesn't do anything
|
||||
* and is perfect as foundation for
|
||||
* other cache drivers and to be used
|
||||
* when the cache is disabled
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class Cache
|
||||
{
|
||||
|
||||
/**
|
||||
* stores all options for the driver
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* Set all parameters which are needed to connect to the cache storage
|
||||
*
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an item to the cache for a given number of minutes.
|
||||
*
|
||||
* <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 void
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to retrieve the cache value
|
||||
* This needs to be defined by the driver
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 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 (time() >= $value->expires()) {
|
||||
$this->remove($key);
|
||||
return $default;
|
||||
}
|
||||
|
||||
// get the pure value
|
||||
$cache = $value->value();
|
||||
|
||||
// return the cache value or the default
|
||||
return $cache ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the expiration timestamp
|
||||
*
|
||||
* @param int $minutes
|
||||
* @return int
|
||||
*/
|
||||
protected function expiration(int $minutes = 0): int
|
||||
{
|
||||
// keep forever if minutes are not defined
|
||||
if ($minutes === 0) {
|
||||
$minutes = 2628000;
|
||||
}
|
||||
|
||||
// calculate the time
|
||||
return time() + ($minutes * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks when an item in the cache expires
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
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 boolean
|
||||
*/
|
||||
public function expired(string $key): bool
|
||||
{
|
||||
return $this->expires($key) <= time();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks when the cache has been created
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
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 mixed
|
||||
*/
|
||||
public function modified(string $key)
|
||||
{
|
||||
return static::created($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Value object
|
||||
*
|
||||
* @param mixed $value The value, which should be cached
|
||||
* @param int $minutes The number of minutes before expiration
|
||||
* @return Value
|
||||
*/
|
||||
protected function value($value, int $minutes): Value
|
||||
{
|
||||
return new Value($value, $minutes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if an item exists in the cache.
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists(string $key): bool
|
||||
{
|
||||
return !$this->expired($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the entire cache
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all passed cache options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function options(): array
|
||||
{
|
||||
return $this->options;
|
||||
}
|
||||
}
|
||||
126
kirby/src/Cache/FileCache.php
Executable file
126
kirby/src/Cache/FileCache.php
Executable file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* File System Cache Driver
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class FileCache extends Cache
|
||||
{
|
||||
|
||||
/**
|
||||
* Set all parameters which are needed for the file cache
|
||||
* see defaults for available parameters
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params)
|
||||
{
|
||||
$defaults = [
|
||||
'root' => null,
|
||||
'extension' => null
|
||||
];
|
||||
|
||||
parent::__construct(array_merge($defaults, $params));
|
||||
|
||||
// try to create the directory
|
||||
Dir::make($this->options['root'], true);
|
||||
|
||||
// check for a valid cache directory
|
||||
if (is_dir($this->options['root']) === false) {
|
||||
throw new Exception('The cache directory does not exist');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path to a file for a given key
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
protected function file(string $key): string
|
||||
{
|
||||
$extension = isset($this->options['extension']) ? '.' . $this->options['extension'] : '';
|
||||
return $this->options['root'] . '/' . $key . $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an item to the cache for a given number of minutes.
|
||||
*
|
||||
* <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
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0)
|
||||
{
|
||||
return F::write($this->file($key), $this->value($value, $minutes)->toJson());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an item from the cache.
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
{
|
||||
return Value::fromJson(F::read($this->file($key)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks when the cache has been created
|
||||
*
|
||||
* @param string $key
|
||||
* @return int
|
||||
*/
|
||||
public function created(string $key): int
|
||||
{
|
||||
// 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)) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
return F::remove($this->file($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the entire cache directory
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
if (Dir::remove($this->options['root']) === true && Dir::make($this->options['root']) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
137
kirby/src/Cache/MemCached.php
Executable file
137
kirby/src/Cache/MemCached.php
Executable file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cache;
|
||||
|
||||
/**
|
||||
* Memcached Driver
|
||||
*
|
||||
* @package Kirby Cache
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class MemCached extends Cache
|
||||
{
|
||||
|
||||
/**
|
||||
* store for the memache connection
|
||||
* @var Memcached
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* Set all parameters which are needed for the memcache client
|
||||
* see defaults for available parameters
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$defaults = [
|
||||
'host' => 'localhost',
|
||||
'port' => 11211,
|
||||
'prefix' => null,
|
||||
];
|
||||
|
||||
parent::__construct(array_merge($defaults, $params));
|
||||
|
||||
$this->connection = new \Memcached();
|
||||
$this->connection->addServer($this->options['host'], $this->options['port']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an item to the cache for a given number of minutes.
|
||||
*
|
||||
* <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 void
|
||||
*/
|
||||
public function set(string $key, $value, int $minutes = 0)
|
||||
{
|
||||
return $this->connection->set($this->key($key), $this->value($value, $minutes)->toJson(), $this->expiration($minutes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full keyname
|
||||
* including the prefix (if set)
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
public function key(string $key): string
|
||||
{
|
||||
return $this->options['prefix'] . $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the CacheValue object from the cache.
|
||||
*
|
||||
* @param string $key
|
||||
* @return object CacheValue
|
||||
*/
|
||||
public function retrieve(string $key)
|
||||
{
|
||||
return Value::fromJson($this->connection->get($this->key($key)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item from the cache
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function remove(string $key): bool
|
||||
{
|
||||
return $this->connection->delete($this->key($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks when an item in the cache expires
|
||||
*
|
||||
* @param string $key
|
||||
* @return int
|
||||
*/
|
||||
public function expires(string $key): int
|
||||
{
|
||||
return parent::expires($this->key($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an item in the cache is expired
|
||||
*
|
||||
* @param string $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function expired(string $key): bool
|
||||
{
|
||||
return parent::expired($this->key($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks when the cache has been created
|
||||
*
|
||||
* @param string $key
|
||||
* @return int
|
||||
*/
|
||||
public function created(string $key): int
|
||||
{
|
||||
return parent::created($this->key($key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the entire cache directory
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function flush(): bool
|
||||
{
|
||||
return $this->connection->flush();
|
||||
}
|
||||
}
|
||||
139
kirby/src/Cache/Value.php
Executable file
139
kirby/src/Cache/Value.php
Executable file
@@ -0,0 +1,139 @@
|
||||
<?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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class Value
|
||||
{
|
||||
|
||||
/**
|
||||
* the cached value
|
||||
* @var mixed
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* the expiration timestamp
|
||||
* @var int
|
||||
*/
|
||||
protected $expires;
|
||||
|
||||
/**
|
||||
* the 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, $created = null)
|
||||
{
|
||||
// keep forever if minutes are not defined
|
||||
if ($minutes === 0) {
|
||||
$minutes = 2628000;
|
||||
}
|
||||
|
||||
$this->value = $value;
|
||||
$this->minutes = $minutes;
|
||||
$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
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function expires(): int
|
||||
{
|
||||
return $this->created + ($this->minutes * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a value object from an array
|
||||
*
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
public static function fromArray(array $array): self
|
||||
{
|
||||
return new static($array['value'] ?? null, $array['minutes'] ?? 0, $array['created'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a value object from a json string
|
||||
*
|
||||
* @param string $json
|
||||
* @return array
|
||||
*/
|
||||
public static function fromJson($json): self
|
||||
{
|
||||
try {
|
||||
$array = json_decode($json, true) ?? [];
|
||||
} catch (Throwable $e) {
|
||||
$array = [];
|
||||
}
|
||||
|
||||
return static::fromArray($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object to a json string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'created' => $this->created,
|
||||
'minutes' => $this->minutes,
|
||||
'value' => $this->value,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function value()
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
176
kirby/src/Cms/Api.php
Executable file
176
kirby/src/Cms/Api.php
Executable file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Api\Api as BaseApi;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
class Api extends BaseApi
|
||||
{
|
||||
protected $kirby;
|
||||
|
||||
public function call(string $path = null, string $method = 'GET', array $requestData = [])
|
||||
{
|
||||
$this->setRequestMethod($method);
|
||||
$this->setRequestData($requestData);
|
||||
|
||||
if ($languageCode = $this->requestHeaders('x-language')) {
|
||||
$this->kirby->setCurrentLanguage($languageCode);
|
||||
}
|
||||
|
||||
if ($user = $this->kirby->user()) {
|
||||
$this->kirby->setCurrentTranslation($user->language());
|
||||
}
|
||||
|
||||
return parent::call($path, $method, $requestData);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
public function file(string $path = null, string $filename)
|
||||
{
|
||||
$filename = urldecode($filename);
|
||||
|
||||
if ($file = $this->parent($path)->file($filename)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => 'file.notFound',
|
||||
'data' => [
|
||||
'filename' => $filename
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function parent(string $path)
|
||||
{
|
||||
$modelType = $path === 'site' ? 'site' : dirname($path);
|
||||
$modelTypes = ['site' => 'site', 'users' => 'user', 'pages' => 'page'];
|
||||
$modelName = $modelTypes[$modelType] ?? null;
|
||||
|
||||
if ($modelName === null) {
|
||||
throw new InvalidArgumentException('Invalid file model type');
|
||||
}
|
||||
|
||||
if ($modelName === 'site') {
|
||||
$modelId = null;
|
||||
} else {
|
||||
$modelId = basename($path);
|
||||
|
||||
if ($modelName === 'page') {
|
||||
$modelId = str_replace('+', '/', $modelId);
|
||||
}
|
||||
}
|
||||
|
||||
if ($model = $this->kirby()->$modelName($modelId)) {
|
||||
return $model;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => $modelName . '.undefined'
|
||||
]);
|
||||
}
|
||||
|
||||
public function kirby()
|
||||
{
|
||||
return $this->kirby;
|
||||
}
|
||||
|
||||
public function language()
|
||||
{
|
||||
return $this->requestHeaders('x-language');
|
||||
}
|
||||
|
||||
public function page(string $id)
|
||||
{
|
||||
$id = str_replace('+', '/', $id);
|
||||
$page = $this->kirby->page($id);
|
||||
|
||||
if ($page && $page->isReadable()) {
|
||||
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));
|
||||
}
|
||||
|
||||
protected function setKirby(App $kirby)
|
||||
{
|
||||
$this->kirby = $kirby;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function site()
|
||||
{
|
||||
return $this->kirby->site();
|
||||
}
|
||||
|
||||
public function user(string $id = null)
|
||||
{
|
||||
// get the authenticated user
|
||||
if ($id === null) {
|
||||
return $this->kirby->auth()->user();
|
||||
}
|
||||
|
||||
// get a specific user by id
|
||||
if ($user = $this->kirby->users()->find($id)) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $id
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->kirby->users();
|
||||
}
|
||||
}
|
||||
1177
kirby/src/Cms/App.php
Executable file
1177
kirby/src/Cms/App.php
Executable file
File diff suppressed because it is too large
Load Diff
112
kirby/src/Cms/AppCaches.php
Executable file
112
kirby/src/Cms/AppCaches.php
Executable file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Cache\Cache;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
trait AppCaches
|
||||
{
|
||||
protected $caches = [];
|
||||
|
||||
/**
|
||||
* Returns a cache instance by key
|
||||
*
|
||||
* @param string $key
|
||||
* @return 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) {
|
||||
return $this->caches[$key] = new Cache;
|
||||
}
|
||||
|
||||
$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
|
||||
return $this->caches[$key] = new $className($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
];
|
||||
}
|
||||
|
||||
$defaults = [
|
||||
'active' => true,
|
||||
'type' => 'file',
|
||||
'extension' => 'cache',
|
||||
'root' => $this->root('cache') . '/' . str_replace('.', '/', $key)
|
||||
];
|
||||
|
||||
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 ($plugin = $this->plugin($pluginName)) {
|
||||
return empty($cacheName) === true ? $pluginPrefix . '.cache' : $pluginPrefix . '.cache.' . $cacheName;
|
||||
}
|
||||
|
||||
return $prefixedKey;
|
||||
}
|
||||
}
|
||||
112
kirby/src/Cms/AppErrors.php
Executable file
112
kirby/src/Cms/AppErrors.php
Executable file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Http\Response;
|
||||
use Whoops\Run as Whoops;
|
||||
use Whoops\Handler\Handler;
|
||||
use Whoops\Handler\PrettyPageHandler;
|
||||
use Whoops\Handler\PlainTextHandler;
|
||||
use Whoops\Handler\CallbackHandler;
|
||||
|
||||
trait AppErrors
|
||||
{
|
||||
protected function handleCliErrors()
|
||||
{
|
||||
$whoops = new Whoops;
|
||||
$whoops->pushHandler(new PlainTextHandler);
|
||||
$whoops->register();
|
||||
}
|
||||
|
||||
protected function handleErrors()
|
||||
{
|
||||
$request = $this->request();
|
||||
|
||||
// TODO: implement acceptance
|
||||
if ($request->ajax()) {
|
||||
return $this->handleJsonErrors();
|
||||
}
|
||||
|
||||
if ($request->cli()) {
|
||||
return $this->handleCliErrors();
|
||||
}
|
||||
|
||||
return $this->handleHtmlErrors();
|
||||
}
|
||||
|
||||
protected function handleHtmlErrors()
|
||||
{
|
||||
$whoops = new Whoops;
|
||||
|
||||
if ($this->option('debug') === true) {
|
||||
if ($this->option('whoops', true) === true) {
|
||||
$handler = new PrettyPageHandler;
|
||||
$handler->setPageTitle('Kirby CMS Debugger');
|
||||
|
||||
if ($editor = $this->option('editor')) {
|
||||
$handler->setEditor($editor);
|
||||
}
|
||||
|
||||
$whoops->pushHandler($handler);
|
||||
$whoops->register();
|
||||
}
|
||||
} else {
|
||||
$handler = new CallbackHandler(function ($exception, $inspector, $run) {
|
||||
$fatal = $this->option('fatal');
|
||||
|
||||
if (is_a($fatal, 'Closure') === true) {
|
||||
echo $fatal($this);
|
||||
} else {
|
||||
include static::$root . '/views/fatal.php';
|
||||
}
|
||||
|
||||
return Handler::QUIT;
|
||||
});
|
||||
|
||||
$whoops->pushHandler($handler);
|
||||
$whoops->register();
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleJsonErrors()
|
||||
{
|
||||
$whoops = new Whoops;
|
||||
$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/options/debug',
|
||||
], $httpCode);
|
||||
}
|
||||
|
||||
return Handler::QUIT;
|
||||
});
|
||||
|
||||
$whoops->pushHandler($handler);
|
||||
$whoops->register();
|
||||
}
|
||||
}
|
||||
507
kirby/src/Cms/AppPlugins.php
Executable file
507
kirby/src/Cms/AppPlugins.php
Executable file
@@ -0,0 +1,507 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Form\Field as FormField;
|
||||
use Kirby\Text\KirbyTag;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Collection;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
trait AppPlugins
|
||||
{
|
||||
|
||||
/**
|
||||
* A list of all registered plugins
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $plugins = [];
|
||||
|
||||
/**
|
||||
* The extension registry
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $extensions = [
|
||||
'api' => [],
|
||||
'blueprints' => [],
|
||||
'cacheTypes' => [],
|
||||
'collections' => [],
|
||||
'components' => [],
|
||||
'controllers' => [],
|
||||
'collectionFilters' => [],
|
||||
'fieldMethods' => [],
|
||||
'fileMethods' => [],
|
||||
'filesMethods' => [],
|
||||
'fields' => [],
|
||||
'hooks' => [],
|
||||
'options' => [],
|
||||
'pages' => [],
|
||||
'pageMethods' => [],
|
||||
'pageModels' => [],
|
||||
'pagesMethods' => [],
|
||||
'routes' => [],
|
||||
'sections' => [],
|
||||
'siteMethods' => [],
|
||||
'snippets' => [],
|
||||
'tags' => [],
|
||||
'templates' => [],
|
||||
'translations' => [],
|
||||
'validators' => []
|
||||
];
|
||||
|
||||
/**
|
||||
* Flag when plugins have been loaded
|
||||
* to not load them again
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $pluginsAreLoaded = false;
|
||||
|
||||
/**
|
||||
* Register all given extensions
|
||||
*
|
||||
* @param array $extensions
|
||||
* @param 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;
|
||||
}
|
||||
|
||||
protected function extendApi($api): array
|
||||
{
|
||||
if (is_array($api) === true) {
|
||||
return $this->extensions['api'] = A::merge($this->extensions['api'], $api, A::MERGE_APPEND);
|
||||
} else {
|
||||
return $this->extensions['api'];
|
||||
}
|
||||
}
|
||||
|
||||
protected function extendBlueprints(array $blueprints): array
|
||||
{
|
||||
return $this->extensions['blueprints'] = array_merge($this->extensions['blueprints'], $blueprints);
|
||||
}
|
||||
|
||||
protected function extendCacheTypes(array $cacheTypes): array
|
||||
{
|
||||
return $this->extensions['cacheTypes'] = array_merge($this->extensions['cacheTypes'], $cacheTypes);
|
||||
}
|
||||
|
||||
protected function extendCollectionFilters(array $filters): array
|
||||
{
|
||||
return $this->extensions['collectionFilters'] = Collection::$filters = array_merge(Collection::$filters, $filters);
|
||||
}
|
||||
|
||||
protected function extendCollections(array $collections): array
|
||||
{
|
||||
return $this->extensions['collections'] = array_merge($this->extensions['collections'], $collections);
|
||||
}
|
||||
|
||||
protected function extendComponents(array $components): array
|
||||
{
|
||||
return $this->extensions['components'] = array_merge($this->extensions['components'], $components);
|
||||
}
|
||||
|
||||
protected function extendControllers(array $controllers): array
|
||||
{
|
||||
return $this->extensions['controllers'] = array_merge($this->extensions['controllers'], $controllers);
|
||||
}
|
||||
|
||||
protected function extendFileMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['fileMethods'] = File::$methods = array_merge(File::$methods, $methods);
|
||||
}
|
||||
|
||||
protected function extendFilesMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['filesMethods'] = Files::$methods = array_merge(Files::$methods, $methods);
|
||||
}
|
||||
|
||||
protected function extendFieldMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['fieldMethods'] = Field::$methods = array_merge(Field::$methods, $methods);
|
||||
}
|
||||
|
||||
protected function extendFields(array $fields): array
|
||||
{
|
||||
return $this->extensions['fields'] = FormField::$types = array_merge(FormField::$types, $fields);
|
||||
}
|
||||
|
||||
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'];
|
||||
}
|
||||
|
||||
protected function extendMarkdown(Closure $markdown): array
|
||||
{
|
||||
return $this->extensions['markdown'] = $markdown;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return $this->extensions['options'] = $this->options = A::merge($options, $this->options, A::MERGE_REPLACE);
|
||||
}
|
||||
|
||||
protected function extendPageMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['pageMethods'] = Page::$methods = array_merge(Page::$methods, $methods);
|
||||
}
|
||||
|
||||
protected function extendPagesMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['pagesMethods'] = Pages::$methods = array_merge(Pages::$methods, $methods);
|
||||
}
|
||||
|
||||
protected function extendPageModels(array $models): array
|
||||
{
|
||||
return $this->extensions['pageModels'] = Page::$models = array_merge(Page::$models, $models);
|
||||
}
|
||||
|
||||
protected function extendPages(array $pages): array
|
||||
{
|
||||
return $this->extensions['pages'] = array_merge($this->extensions['pages'], $pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional routes
|
||||
*
|
||||
* @param array|Closure $routes
|
||||
* @return array
|
||||
*/
|
||||
protected function extendRoutes($routes): array
|
||||
{
|
||||
if (is_a($routes, Closure::class) === true) {
|
||||
$routes = $routes($this);
|
||||
}
|
||||
|
||||
return $this->extensions['routes'] = array_merge($this->extensions['routes'], $routes);
|
||||
}
|
||||
|
||||
protected function extendSections(array $sections): array
|
||||
{
|
||||
return $this->extensions['sections'] = Section::$types = array_merge(Section::$types, $sections);
|
||||
}
|
||||
|
||||
protected function extendSiteMethods(array $methods): array
|
||||
{
|
||||
return $this->extensions['siteMethods'] = Site::$methods = array_merge(Site::$methods, $methods);
|
||||
}
|
||||
|
||||
protected function extendSmartypants(Closure $smartypants): array
|
||||
{
|
||||
return $this->extensions['smartypants'] = $smartypants;
|
||||
}
|
||||
|
||||
protected function extendSnippets(array $snippets): array
|
||||
{
|
||||
return $this->extensions['snippets'] = array_merge($this->extensions['snippets'], $snippets);
|
||||
}
|
||||
|
||||
protected function extendTags(array $tags): array
|
||||
{
|
||||
return $this->extensions['tags'] = KirbyTag::$types = array_merge(KirbyTag::$types, $tags);
|
||||
}
|
||||
|
||||
protected function extendTemplates(array $templates): array
|
||||
{
|
||||
return $this->extensions['templates'] = array_merge($this->extensions['templates'], $templates);
|
||||
}
|
||||
|
||||
protected function extendTranslations(array $translations): array
|
||||
{
|
||||
return $this->extensions['translations'] = array_replace_recursive($this->extensions['translations'], $translations);
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @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
|
||||
include_once $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
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function extensionsFromProps(array $props)
|
||||
{
|
||||
$this->extend($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all default extensions
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function extensionsFromSystem()
|
||||
{
|
||||
// Form Field Mixins
|
||||
FormField::$mixins['options'] = include static::$root . '/config/fields/mixins/options.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',
|
||||
'link' => 'toLink',
|
||||
'md' => 'markdown',
|
||||
'sp' => 'smartypants',
|
||||
'v' => 'isValid',
|
||||
'x' => 'xml'
|
||||
];
|
||||
|
||||
// default cache types
|
||||
$this->extendCacheTypes([
|
||||
'apcu' => 'Kirby\Cache\ApcuCache',
|
||||
'file' => 'Kirby\Cache\FileCache',
|
||||
'memcached' => 'Kirby\Cache\MemCache',
|
||||
]);
|
||||
|
||||
$this->extendComponents(include static::$root . '/config/components.php');
|
||||
$this->extendBlueprints(include static::$root . '/config/blueprints.php');
|
||||
$this->extendFields(include static::$root . '/config/fields.php');
|
||||
$this->extendFieldMethods((include static::$root . '/config/methods.php')($this));
|
||||
$this->extendTags(include static::$root . '/config/tags.php');
|
||||
|
||||
// blueprint presets
|
||||
PageBlueprint::$presets['pages'] = include static::$root . '/config/presets/pages.php';
|
||||
PageBlueprint::$presets['page'] = include static::$root . '/config/presets/page.php';
|
||||
PageBlueprint::$presets['files'] = include static::$root . '/config/presets/files.php';
|
||||
|
||||
// section mixins
|
||||
Section::$mixins['headline'] = include static::$root . '/config/sections/mixins/headline.php';
|
||||
Section::$mixins['layout'] = include static::$root . '/config/sections/mixins/layout.php';
|
||||
Section::$mixins['max'] = include static::$root . '/config/sections/mixins/max.php';
|
||||
Section::$mixins['min'] = include static::$root . '/config/sections/mixins/min.php';
|
||||
Section::$mixins['pagination'] = include static::$root . '/config/sections/mixins/pagination.php';
|
||||
Section::$mixins['parent'] = include static::$root . '/config/sections/mixins/parent.php';
|
||||
|
||||
// section types
|
||||
Section::$types['info'] = include static::$root . '/config/sections/info.php';
|
||||
Section::$types['pages'] = include static::$root . '/config/sections/pages.php';
|
||||
Section::$types['files'] = include static::$root . '/config/sections/files.php';
|
||||
Section::$types['fields'] = include static::$root . '/config/sections/fields.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
*
|
||||
* @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');
|
||||
$kirby = $this;
|
||||
$loaded = [];
|
||||
|
||||
foreach (Dir::read($root) as $dirname) {
|
||||
if (in_array(substr($dirname, 0, 1), ['.', '_']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($root . '/' . $dirname) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$dir = $root . '/' . $dirname;
|
||||
$entry = $dir . '/index.php';
|
||||
|
||||
if (file_exists($entry) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
include_once $entry;
|
||||
|
||||
$loaded[] = $dir;
|
||||
}
|
||||
|
||||
return $loaded;
|
||||
}
|
||||
}
|
||||
127
kirby/src/Cms/AppTranslations.php
Executable file
127
kirby/src/Cms/AppTranslations.php
Executable file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\I18n;
|
||||
|
||||
trait AppTranslations
|
||||
{
|
||||
protected $translations;
|
||||
|
||||
/**
|
||||
* Setup internationalization
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function i18n()
|
||||
{
|
||||
I18n::$load = function ($locale) {
|
||||
$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());
|
||||
}
|
||||
|
||||
return $data;
|
||||
};
|
||||
|
||||
I18n::$locale = function () {
|
||||
if ($this->multilang() === true) {
|
||||
return $this->defaultLanguage()->code();
|
||||
} else {
|
||||
return 'en';
|
||||
}
|
||||
};
|
||||
|
||||
I18n::$fallback = function () {
|
||||
if ($this->multilang() === true) {
|
||||
return $this->defaultLanguage()->code();
|
||||
} else {
|
||||
return 'en';
|
||||
}
|
||||
};
|
||||
|
||||
I18n::$translations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and set the current language if it exists
|
||||
* Otherwise fall back to the default language
|
||||
*
|
||||
* @param string $languageCode
|
||||
* @return Language|null
|
||||
*/
|
||||
public function setCurrentLanguage(string $languageCode = null)
|
||||
{
|
||||
if ($languageCode === null) {
|
||||
return $this->language = null;
|
||||
}
|
||||
|
||||
if ($language = $this->language($languageCode)) {
|
||||
$this->language = $language;
|
||||
} else {
|
||||
$this->language = $this->defaultLanguage();
|
||||
}
|
||||
|
||||
if ($this->language) {
|
||||
setlocale(LC_ALL, $this->language->locale());
|
||||
}
|
||||
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current translation
|
||||
*
|
||||
* @param string $translationCode
|
||||
* @return void
|
||||
*/
|
||||
public function setCurrentTranslation(string $translationCode = null)
|
||||
{
|
||||
I18n::$locale = $translationCode ?? 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specific translation by locale
|
||||
*
|
||||
* @param string|null $locale
|
||||
* @return 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('translations') . '/' . $locale . '.json', $inject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available translations
|
||||
*
|
||||
* @return Translations
|
||||
*/
|
||||
public function translations()
|
||||
{
|
||||
if (is_a($this->translations, 'Kirby\Cms\Translations') === true) {
|
||||
return $this->translations;
|
||||
}
|
||||
|
||||
return Translations::load($this->root('translations'), $this->extensions['translations'] ?? []);
|
||||
}
|
||||
}
|
||||
99
kirby/src/Cms/AppUsers.php
Executable file
99
kirby/src/Cms/AppUsers.php
Executable file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
trait AppUsers
|
||||
{
|
||||
|
||||
/**
|
||||
* Cache for the auth auth layer
|
||||
*
|
||||
* @var Auth
|
||||
*/
|
||||
protected $auth;
|
||||
|
||||
/**
|
||||
* Returns the Authentication layer class
|
||||
*
|
||||
* @return Auth
|
||||
*/
|
||||
public function auth()
|
||||
{
|
||||
return $this->auth = $this->auth ?? new Auth($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Become any existing user
|
||||
*
|
||||
* @param string|null $who
|
||||
* @return self
|
||||
*/
|
||||
public function impersonate(string $who = null)
|
||||
{
|
||||
return $this->auth()->impersonate($who);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currently active user id
|
||||
*
|
||||
* @param User|string $user
|
||||
* @return self
|
||||
*/
|
||||
protected function setUser($user = null): self
|
||||
{
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create your own set of app users
|
||||
*
|
||||
* @param array $users
|
||||
* @return self
|
||||
*/
|
||||
protected function setUsers(array $users = null): self
|
||||
{
|
||||
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 $id
|
||||
* @param \Kirby\Session\Session|array $session Session options or session object for getting the current user
|
||||
* @return User|null
|
||||
*/
|
||||
public function user(string $id = null, $session = null)
|
||||
{
|
||||
if ($id !== null) {
|
||||
return $this->users()->find($id);
|
||||
}
|
||||
|
||||
if (is_string($this->user) === true) {
|
||||
return $this->auth()->impersonate($this->user);
|
||||
} else {
|
||||
return $this->auth()->user();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all users
|
||||
*
|
||||
* @return Users
|
||||
*/
|
||||
public function users(): Users
|
||||
{
|
||||
if (is_a($this->users, 'Kirby\Cms\Users') === true) {
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
return $this->users = Users::load($this->root('accounts'), ['kirby' => $this]);
|
||||
}
|
||||
}
|
||||
11
kirby/src/Cms/Asset.php
Executable file
11
kirby/src/Cms/Asset.php
Executable file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
||||
class Asset
|
||||
{
|
||||
use FileFoundation;
|
||||
use Properties;
|
||||
}
|
||||
333
kirby/src/Cms/Auth.php
Executable file
333
kirby/src/Cms/Auth.php
Executable file
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Http\Request\Auth\BasicAuth;
|
||||
use Kirby\Session\Session;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Authentication layer
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
protected $impersonate;
|
||||
protected $kirby;
|
||||
protected $user;
|
||||
|
||||
/**
|
||||
* @param App $kirby
|
||||
*/
|
||||
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 BasicAuth|null $auth
|
||||
* @return 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
|
||||
if ($request->ssl() === false) {
|
||||
throw new PermissionException('Basic authentication is only allowed over HTTPS');
|
||||
}
|
||||
|
||||
if ($user = $this->kirby->users()->find($auth->username())) {
|
||||
if ($user->validatePassword($auth->password()) === true) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logged in user by checking
|
||||
* the current session and finding a valid
|
||||
* valid user id in there
|
||||
*
|
||||
* @param Session|null $session
|
||||
* @return User|null
|
||||
*/
|
||||
public function currentUserFromSession($session = null)
|
||||
{
|
||||
// use passed session options or session object if set
|
||||
if (is_array($session)) {
|
||||
$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
|
||||
* @return 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
|
||||
{
|
||||
return hash('sha256', $this->kirby->visitor()->ip());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if logins are blocked for the current ip
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isBlocked(): bool
|
||||
{
|
||||
$ip = $this->ipHash();
|
||||
$log = $this->log();
|
||||
$trials = $this->kirby->option('auth.trials', 10);
|
||||
$timeout = $this->kirby->option('auth.timeout', 3600);
|
||||
|
||||
if ($entry = ($log[$ip] ?? null)) {
|
||||
if ($entry['trials'] > $trials) {
|
||||
if ($entry['time'] > (time() - $timeout)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login a user by email and password
|
||||
*
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
* @param boolean $long
|
||||
* @return User|false
|
||||
*/
|
||||
public function login(string $email, string $password, bool $long = false)
|
||||
{
|
||||
// check for blocked ips
|
||||
if ($this->isBlocked() === true) {
|
||||
throw new PermissionException('Rate limit exceeded', 403);
|
||||
}
|
||||
|
||||
// stop impersonating
|
||||
$this->impersonate = null;
|
||||
|
||||
// session options
|
||||
$options = [
|
||||
'createMode' => 'cookie',
|
||||
'long' => $long === true
|
||||
];
|
||||
|
||||
// validate the user and log in to the session
|
||||
if ($user = $this->kirby->users()->find($email)) {
|
||||
if ($user->login($password, $options) === true) {
|
||||
return $this->user = $user;
|
||||
}
|
||||
}
|
||||
|
||||
// log invalid login trial
|
||||
$this->track();
|
||||
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder
|
||||
usleep(random_int(1000, 2000000));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
return Data::read($this->logfile(), 'json');
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the current user
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function logout(): bool
|
||||
{
|
||||
// stop impersonating
|
||||
$this->impersonate = null;
|
||||
|
||||
// logout the current user if it exists
|
||||
if ($user = $this->user()) {
|
||||
$user->logout();
|
||||
}
|
||||
|
||||
$this->user = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks a login
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function track(): bool
|
||||
{
|
||||
$ip = $this->ipHash();
|
||||
$log = $this->log();
|
||||
$time = time();
|
||||
|
||||
if (isset($log[$ip]) === true) {
|
||||
$log[$ip] = [
|
||||
'time' => $time,
|
||||
'trials' => ($log[$ip]['trials'] ?? 0) + 1
|
||||
];
|
||||
} else {
|
||||
$log[$ip] = [
|
||||
'time' => $time,
|
||||
'trials' => 1
|
||||
];
|
||||
}
|
||||
|
||||
return Data::write($this->logfile(), $log, 'json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current authentication type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
$basicAuth = $this->kirby->option('api.basicAuth', false);
|
||||
$auth = $this->kirby->request()->auth();
|
||||
|
||||
if ($basicAuth === true && $auth && $auth->type() === 'basic') {
|
||||
return 'basic';
|
||||
} elseif ($this->impersonate !== null) {
|
||||
return 'impersonate';
|
||||
} else {
|
||||
return 'session';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the currently logged in user
|
||||
*
|
||||
* @param array|Session|null $session
|
||||
* @return User|null
|
||||
*/
|
||||
public function user($session = null): ?User
|
||||
{
|
||||
if ($this->impersonate !== null) {
|
||||
return $this->impersonate;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->type() === 'basic') {
|
||||
return $this->user = $this->currentUserFromBasicAuth();
|
||||
} else {
|
||||
return $this->user = $this->currentUserFromSession($session);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
return $this->user = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
759
kirby/src/Cms/Blueprint.php
Executable file
759
kirby/src/Cms/Blueprint.php
Executable file
@@ -0,0 +1,759 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Form\Field;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Obj;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The Blueprint class normalizes an array from a
|
||||
* blueprint file and converts sections, columns, fields
|
||||
* etc. into a correct tab layout.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
$mixin = static::find($extends);
|
||||
|
||||
if ($mixin === null) {
|
||||
$props = $props;
|
||||
} elseif (is_array($mixin) === true) {
|
||||
$props = array_replace_recursive($mixin, $props);
|
||||
} else {
|
||||
try {
|
||||
$props = array_replace_recursive(Data::read($mixin), $props);
|
||||
} catch (Exception $e) {
|
||||
$props = $props;
|
||||
}
|
||||
}
|
||||
|
||||
// remove the extends flag
|
||||
unset($props['extends']);
|
||||
return $props;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new blueprint for a model
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $fallback
|
||||
* @param 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 string|array
|
||||
*/
|
||||
public static function find(string $name)
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$root = $kirby->root('blueprints');
|
||||
$file = $root . '/' . $name . '.yml';
|
||||
|
||||
if (F::exists($file, $root) === true) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
if ($blueprint = $kirby->extension('blueprints', $name)) {
|
||||
return $blueprint;
|
||||
}
|
||||
|
||||
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
|
||||
* @param string $fallback
|
||||
* @param Model $model
|
||||
* @return array
|
||||
*/
|
||||
public static function load(string $name)
|
||||
{
|
||||
if (isset(static::$loaded[$name]) === true) {
|
||||
return static::$loaded[$name];
|
||||
}
|
||||
|
||||
$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;
|
||||
};
|
||||
|
||||
if (is_array($props) === true) {
|
||||
return $normalize($props);
|
||||
}
|
||||
|
||||
$file = $props;
|
||||
$props = Data::read($file);
|
||||
|
||||
return static::$loaded[$name] = $normalize($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model
|
||||
*
|
||||
* @return 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) {
|
||||
$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)
|
||||
{
|
||||
$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' => $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 = [];
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
// inject all section extensions
|
||||
$sectionProps = $this->extend($sectionProps);
|
||||
|
||||
$sections[$sectionName] = $sectionProps = array_merge($sectionProps, [
|
||||
'name' => $sectionName,
|
||||
'type' => $type = $sectionProps['type'] ?? null
|
||||
]);
|
||||
|
||||
if (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) {
|
||||
|
||||
// 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 Section|null
|
||||
*/
|
||||
public function section(string $name): ?Section
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
271
kirby/src/Cms/Collection.php
Executable file
271
kirby/src/Cms/Collection.php
Executable file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
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)) {
|
||||
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[$object->id()] = $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single object or
|
||||
* an entire second collection to the
|
||||
* current collection
|
||||
*
|
||||
* @param mixed $item
|
||||
*/
|
||||
public function add($object)
|
||||
{
|
||||
if (is_a($object, static::class) === true) {
|
||||
$this->data = array_merge($this->data, $object->data);
|
||||
} else {
|
||||
$this->__set($object->id(), $object);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an element to the data array
|
||||
*
|
||||
* @param mixed $key
|
||||
* @param mixed $item
|
||||
* @return Collection
|
||||
*/
|
||||
public function append(...$args)
|
||||
{
|
||||
if (count($args) === 1) {
|
||||
if (is_object($args[0]) === true) {
|
||||
$this->data[$args[0]->id()] = $args[0];
|
||||
} else {
|
||||
$this->data[] = $args[0];
|
||||
}
|
||||
} elseif (count($args) === 2) {
|
||||
$this->set($args[0], $args[1]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the items by a given field
|
||||
*
|
||||
* @param string $field
|
||||
* @param bool $i (ignore upper/lowercase for group names)
|
||||
* @return Collection A collection with an item for each group and a Collection for each group
|
||||
*/
|
||||
public function groupBy(string $field, bool $i = true)
|
||||
{
|
||||
$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
|
||||
* @return boolean
|
||||
*/
|
||||
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 args any number of keys, passed as individual arguments
|
||||
* @return 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
|
||||
*
|
||||
* @return Collection a sliced set of data
|
||||
*/
|
||||
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 Model
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
121
kirby/src/Cms/Collections.php
Executable file
121
kirby/src/Cms/Collections.php
Executable file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Toolkit\Controller;
|
||||
|
||||
/**
|
||||
* 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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
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 Collection|null
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
return $this->get($name, ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Collections set
|
||||
*
|
||||
* @param array $collections
|
||||
*/
|
||||
public function __construct(array $collections = [])
|
||||
{
|
||||
$this->collections = $collections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a collection by name if registered
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $data
|
||||
* @return Collection|null
|
||||
*/
|
||||
public function get(string $name, array $data = [])
|
||||
{
|
||||
if (isset($this->cache[$name]) === true) {
|
||||
return $this->cache[$name];
|
||||
}
|
||||
|
||||
if (isset($this->collections[$name]) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$controller = new Controller($this->collections[$name]);
|
||||
|
||||
return $this->cache[$name] = $controller->call(null, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a collection exists
|
||||
*
|
||||
* @param string $name
|
||||
* @return boolean
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return isset($this->collections[$name]) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads collections from php files in a
|
||||
* given directory.
|
||||
*
|
||||
* @param string $root
|
||||
* @return self
|
||||
*/
|
||||
public static function load(App $app): self
|
||||
{
|
||||
$collections = $app->extensions('collections');
|
||||
$root = $app->root('collections');
|
||||
|
||||
foreach (glob($root . '/*.php') as $file) {
|
||||
$collection = require $file;
|
||||
|
||||
if (is_a($collection, 'Closure')) {
|
||||
$name = pathinfo($file, PATHINFO_FILENAME);
|
||||
$collections[$name] = $collection;
|
||||
}
|
||||
}
|
||||
|
||||
return new static($collections);
|
||||
}
|
||||
}
|
||||
222
kirby/src/Cms/Content.php
Executable file
222
kirby/src/Cms/Content.php
Executable file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* The Content class handles all fields
|
||||
* for content from pages, the site and users
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
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 Page|File|User|Site
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* Magic getter for content fields
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return Field
|
||||
*/
|
||||
public function __call(string $name, array $arguments = []): Field
|
||||
{
|
||||
return $this->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Content object
|
||||
*
|
||||
* @param array $data
|
||||
* @param object $parent
|
||||
*/
|
||||
public function __construct($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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 boolean
|
||||
*/
|
||||
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): self
|
||||
{
|
||||
$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 Site|Page|File|User
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parent model
|
||||
*
|
||||
* @param Model $parent
|
||||
* @return self
|
||||
*/
|
||||
public function setParent(Model $parent): self
|
||||
{
|
||||
$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): self
|
||||
{
|
||||
$this->data = $overwrite === true ? (array)$content : array_merge($this->data, (array)$content);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
230
kirby/src/Cms/ContentTranslation.php
Executable file
230
kirby/src/Cms/ContentTranslation.php
Executable file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
||||
/**
|
||||
* Each page, file or site can have multiple
|
||||
* translated versions of their content,
|
||||
* represented by this class
|
||||
*/
|
||||
class ContentTranslation
|
||||
{
|
||||
use Properties;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $contentFile;
|
||||
|
||||
/**
|
||||
* @var Page|Site|File
|
||||
*/
|
||||
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();
|
||||
$content = $this->content ?? $parent->readContent($this->code());
|
||||
|
||||
// merge with the default content
|
||||
if ($this->isDefault() === false && $defaultLanguage = $parent->kirby()->defaultLanguage()) {
|
||||
$default = $parent->translation($defaultLanguage->code())->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 boolean
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->contentFile()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translation code as id
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function id()
|
||||
{
|
||||
return $this->code();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the this is the default translation
|
||||
* of the model
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
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 Page|File|Site
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @return self
|
||||
*/
|
||||
protected function setCode(string $code): self
|
||||
{
|
||||
$this->code = $code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $content
|
||||
* @return self
|
||||
*/
|
||||
protected function setContent(array $content = null): self
|
||||
{
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Model $parent
|
||||
* @return self
|
||||
*/
|
||||
protected function setParent(Model $parent): self
|
||||
{
|
||||
$this->parent = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $slug
|
||||
* @return self
|
||||
*/
|
||||
protected function setSlug(string $slug = null): self
|
||||
{
|
||||
$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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
177
kirby/src/Cms/Dir.php
Executable file
177
kirby/src/Cms/Dir.php
Executable file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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 boolean $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($dir, $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(string $dir, 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;
|
||||
}
|
||||
}
|
||||
140
kirby/src/Cms/Email.php
Executable file
140
kirby/src/Cms/Email.php
Executable file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Toolkit\Tpl;
|
||||
|
||||
/**
|
||||
* Wrapper around our PHPMailer package, which
|
||||
* handles all the magic connections between Kirby
|
||||
* and sending emails, like email templates, file
|
||||
* attachments, etc.
|
||||
*/
|
||||
class Email
|
||||
{
|
||||
protected $options;
|
||||
protected $preset;
|
||||
protected $props;
|
||||
|
||||
protected static $transform = [
|
||||
'from' => 'user',
|
||||
'replyTo' => 'user',
|
||||
'to' => 'user',
|
||||
'cc' => 'user',
|
||||
'bcc' => 'user',
|
||||
'attachments' => 'file'
|
||||
];
|
||||
|
||||
public function __construct($preset = [], array $props = [])
|
||||
{
|
||||
$this->options = $options = App::instance()->option('email');
|
||||
|
||||
// load presets from options
|
||||
$this->preset = $this->preset($preset);
|
||||
$this->props = array_merge($this->preset, $props);
|
||||
|
||||
// add transport settings
|
||||
$this->props['transport'] = $this->options['transport'] ?? [];
|
||||
|
||||
// transform model objects to values
|
||||
foreach (static::$transform as $prop => $model) {
|
||||
$this->transformProp($prop, $model);
|
||||
}
|
||||
|
||||
// load template for body text
|
||||
$this->template();
|
||||
}
|
||||
|
||||
protected function preset($preset)
|
||||
{
|
||||
// only passed props, not preset name
|
||||
if (is_string($preset) !== true) {
|
||||
return $preset;
|
||||
}
|
||||
|
||||
// preset does not exist
|
||||
if (isset($this->options['presets'][$preset]) === false) {
|
||||
throw new NotFoundException([
|
||||
'key' => 'email.preset.notFound',
|
||||
'data' => ['name' => $preset]
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->options['presets'][$preset];
|
||||
}
|
||||
|
||||
protected function template()
|
||||
{
|
||||
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() && $text->exists()) {
|
||||
$this->props['body'] = [
|
||||
'html' => $html->render($data),
|
||||
'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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTemplate(string $name, string $type = null)
|
||||
{
|
||||
return App::instance()->template('emails/' . $name, $type, 'text');
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->props;
|
||||
}
|
||||
|
||||
protected function transformFile($file)
|
||||
{
|
||||
return $this->transformModel($file, 'Kirby\Cms\File', 'root');
|
||||
}
|
||||
|
||||
protected function transformModel($value, $class, $content)
|
||||
{
|
||||
// value is already a string
|
||||
if (is_string($value) === true) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// value is a model object, get value through content method
|
||||
if (is_a($value, $class) === true) {
|
||||
return $value->$content();
|
||||
}
|
||||
|
||||
// value is an array or collection, call transform on each item
|
||||
if (is_array($value) === true || is_a($value, 'Kirby\Cms\Collection') === true) {
|
||||
$models = [];
|
||||
foreach ($value as $model) {
|
||||
$models[] = $this->transformModel($model, $class, $content);
|
||||
}
|
||||
return $models;
|
||||
}
|
||||
}
|
||||
|
||||
protected function transformProp($prop, $model)
|
||||
{
|
||||
if (isset($this->props[$prop]) === true) {
|
||||
$this->props[$prop] = $this->{'transform' . ucfirst($model)}($this->props[$prop]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function transformUser($user)
|
||||
{
|
||||
return $this->transformModel($user, 'Kirby\Cms\User', 'email');
|
||||
}
|
||||
}
|
||||
246
kirby/src/Cms/Field.php
Executable file
246
kirby/src/Cms/Field.php
Executable file
@@ -0,0 +1,246 @@
|
||||
<?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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
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 Site|Page|File|User
|
||||
*/
|
||||
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 = [])
|
||||
{
|
||||
if (isset(static::$methods[$method]) === true) {
|
||||
return static::$methods[$method](clone $this, ...$arguments);
|
||||
}
|
||||
|
||||
if (isset(static::$aliases[$method]) === true) {
|
||||
$method = 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 boolean
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return $this->parent->content()->has($this->key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field content is empty
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->value) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field content is not empty
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isNotEmpty(): bool
|
||||
{
|
||||
return empty($this->value) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the field
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key(): string
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Page|File|Site|User
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param string|Closure $value
|
||||
* @return mixed If a new value is passed, the modified
|
||||
* field will be returned. Otherwise it
|
||||
* will return the field value.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
867
kirby/src/Cms/File.php
Executable file
867
kirby/src/Cms/File.php
Executable file
@@ -0,0 +1,867 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
class File extends ModelWithContent
|
||||
{
|
||||
use FileActions;
|
||||
use FileFoundation;
|
||||
use HasMethods;
|
||||
use HasSiblings;
|
||||
|
||||
/**
|
||||
* The parent asset object
|
||||
* This is used to do actual file
|
||||
* method calls, like size, mime, etc.
|
||||
*
|
||||
* @var Image
|
||||
*/
|
||||
protected $asset;
|
||||
|
||||
/**
|
||||
* Cache for the initialized blueprint object
|
||||
*
|
||||
* @var FileBlueprint
|
||||
*/
|
||||
protected $blueprint;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $filename;
|
||||
|
||||
/**
|
||||
* All registered file methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $methods = [];
|
||||
|
||||
/**
|
||||
* The parent object
|
||||
*
|
||||
* @var 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
|
||||
*
|
||||
* @param bool $relative
|
||||
* @return string
|
||||
*/
|
||||
public function apiUrl(bool $relative = false): string
|
||||
{
|
||||
return $this->parent()->apiUrl($relative) . '/files/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Asset object
|
||||
*
|
||||
* @return Image
|
||||
*/
|
||||
public function asset(): Image
|
||||
{
|
||||
return $this->asset = $this->asset ?? new Image($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the FileBlueprint object for the file
|
||||
*
|
||||
* @return FileBlueprint
|
||||
*/
|
||||
public function blueprint(): FileBlueprint
|
||||
{
|
||||
if (is_a($this->blueprint, 'Kirby\Cms\FileBlueprint') === true) {
|
||||
return $this->blueprint;
|
||||
}
|
||||
|
||||
return $this->blueprint = FileBlueprint::factory('files/' . $this->template(), 'files/default', $this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blurs the image by the given amount of pixels
|
||||
*
|
||||
* @param boolean $pixels
|
||||
* @return self
|
||||
*/
|
||||
public function blur($pixels = true)
|
||||
{
|
||||
return $this->thumb(['blur' => $pixels]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the image to black and white
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function bw()
|
||||
{
|
||||
return $this->thumb(['grayscale' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the template in addition to the
|
||||
* other content.
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileDirectory(): string
|
||||
{
|
||||
return dirname($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename for the content file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileName(): string
|
||||
{
|
||||
return $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crops the image by the given width and height
|
||||
*
|
||||
* @param integer $width
|
||||
* @param integer $height
|
||||
* @param string|array $options
|
||||
* @return self
|
||||
*/
|
||||
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
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a kirbytag or markdown
|
||||
* tag for the file, which will be
|
||||
* used in the panel, when the file
|
||||
* gets dragged onto a textarea
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dragText($type = 'kirbytext'): string
|
||||
{
|
||||
switch ($type) {
|
||||
case 'kirbytext':
|
||||
if ($this->type() === 'image') {
|
||||
return '(image: ' . $this->filename() . ')';
|
||||
} else {
|
||||
return '(file: ' . $this->filename() . ')';
|
||||
}
|
||||
// no break
|
||||
case 'markdown':
|
||||
if ($this->type() === 'image') {
|
||||
return ' . ')';
|
||||
} else {
|
||||
return '[' . $this->filename() . '](./' . $this->filename() . ')';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file exists on disk
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return is_file($this->root()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename with extension
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function filename(): string
|
||||
{
|
||||
return $this->filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Files collection
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
public function files(): Files
|
||||
{
|
||||
return $this->siblingsCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 File $file
|
||||
* @return bool
|
||||
*/
|
||||
public function is(File $file): bool
|
||||
{
|
||||
return $this->id() === $file->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a unique media hash
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaHash(): string
|
||||
{
|
||||
return crc32($this->filename()) . '-' . $this->modified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file in the public media folder
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return $this->parent()->mediaRoot() . '/' . $this->mediaHash() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute Url to the file in the public media folder
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
return $this->parent()->mediaUrl() . '/' . $this->mediaHash() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for the old way of fetching File
|
||||
* content. Nowadays `File::content()` should
|
||||
* be used instead.
|
||||
*
|
||||
* @return Content
|
||||
*/
|
||||
public function meta(): Content
|
||||
{
|
||||
return $this->content();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model.
|
||||
* This is normally the parent page
|
||||
* or the site object.
|
||||
*
|
||||
* @return Site|Page
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return $this->parent();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 parent Page object
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
return is_a($this->parent(), 'Kirby\Cms\Page') === true ? $this->parent() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Panel icon definition
|
||||
*
|
||||
* @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()] ?? []);
|
||||
|
||||
$settings = [
|
||||
'type' => $definition['type'] ?? 'file',
|
||||
'back' => 'pattern',
|
||||
'color' => $definition['color'] ?? $colorWhite,
|
||||
'ratio' => $params['ratio'] ?? null,
|
||||
];
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Panel image definition
|
||||
*
|
||||
* @param string|array|false $settings
|
||||
* @param array $thumbSettings
|
||||
* @return array
|
||||
*/
|
||||
public function panelImage($settings = null, array $thumbSettings = null): ?array
|
||||
{
|
||||
$defaults = [
|
||||
'ratio' => '3/2',
|
||||
'back' => 'pattern',
|
||||
'cover' => false
|
||||
];
|
||||
|
||||
// switch the image off
|
||||
if ($settings === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($settings) === true) {
|
||||
$settings = [
|
||||
'query' => $settings
|
||||
];
|
||||
}
|
||||
|
||||
$image = $this->query($settings['query'] ?? null, 'Kirby\Cms\File');
|
||||
|
||||
if ($image === null && $this->isViewable() === true) {
|
||||
$image = $this;
|
||||
}
|
||||
|
||||
if ($image) {
|
||||
$settings['url'] = $image->thumb($thumbSettings)->url(true);
|
||||
unset($settings['query']);
|
||||
}
|
||||
|
||||
return array_merge($defaults, (array)$settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
return 'files/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @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 Model
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent = $this->parent ?? $this->kirby()->site();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent id if a parent exists
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function parentId(): ?string
|
||||
{
|
||||
if ($parent = $this->parent()) {
|
||||
return $parent->id();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a collection of all parent pages
|
||||
*
|
||||
* @return Pages
|
||||
*/
|
||||
public function parents(): Pages
|
||||
{
|
||||
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 FilePermissions
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return new FilePermissions($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JPEG compression quality
|
||||
*
|
||||
* @param integer $quality
|
||||
* @return self
|
||||
*/
|
||||
public function quality(int $quality)
|
||||
{
|
||||
return $this->thumb(['quality' => $quality]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string query, starting from the model
|
||||
*
|
||||
* @param string|null $query
|
||||
* @param string|null $expect
|
||||
* @return mixed
|
||||
*/
|
||||
public function query(string $query = null, string $expect = null)
|
||||
{
|
||||
if ($query === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = Str::query($query, [
|
||||
'kirby' => $this->kirby(),
|
||||
'site' => $this->site(),
|
||||
'file' => $this
|
||||
]);
|
||||
|
||||
if ($expect !== null && is_a($result, $expect) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the file with the given width and height
|
||||
* while keeping the aspect ratio.
|
||||
*
|
||||
* @param integer $width
|
||||
* @param integer $height
|
||||
* @param integer $quality
|
||||
* @return self
|
||||
*/
|
||||
public function resize(int $width = null, int $height = null, int $quality = null)
|
||||
{
|
||||
return $this->thumb([
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'quality' => $quality
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 FileRules
|
||||
*/
|
||||
protected function rules()
|
||||
{
|
||||
return new FileRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Blueprint object
|
||||
*
|
||||
* @param array|null $blueprint
|
||||
* @return self
|
||||
*/
|
||||
protected function setBlueprint(array $blueprint = null): self
|
||||
{
|
||||
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): self
|
||||
{
|
||||
$this->filename = $filename;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent model object
|
||||
*
|
||||
* @param Model $parent
|
||||
* @return self
|
||||
*/
|
||||
protected function setParent(Model $parent = null): self
|
||||
{
|
||||
$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): self
|
||||
{
|
||||
$this->template = $template;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the url
|
||||
*
|
||||
* @param string $url
|
||||
* @return self
|
||||
*/
|
||||
protected function setUrl(string $url = null): self
|
||||
{
|
||||
$this->url = $url;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Files collection
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
protected function siblingsCollection()
|
||||
{
|
||||
return $this->parent()->files();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Site object
|
||||
*
|
||||
* @return Site
|
||||
*/
|
||||
public function site(): 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 self
|
||||
*/
|
||||
public function templateSiblings(bool $self = true)
|
||||
{
|
||||
return $this->siblings($self)->filterBy('template', $this->template());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $options
|
||||
* @return FileVersion|File
|
||||
*/
|
||||
public function thumb(array $options = null)
|
||||
{
|
||||
if (empty($options) === true) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$result = $this->kirby()->component('file::version')($this->kirby(), $this, $options);
|
||||
|
||||
if (is_a($result, FileVersion::class) === false && is_a($result, File::class) === false) {
|
||||
throw new InvalidArgumentException('The file::version component must return a File or FileVersion object');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
|
||||
/**
|
||||
* String template builder
|
||||
*
|
||||
* @param string|null $template
|
||||
* @return string
|
||||
*/
|
||||
public function toString(string $template = null): string
|
||||
{
|
||||
if ($template === null) {
|
||||
return $this->id();
|
||||
}
|
||||
|
||||
return Str::template($template, [
|
||||
'file' => $this,
|
||||
'site' => $this->site(),
|
||||
'kirby' => $this->kirby()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Url
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function url(): string
|
||||
{
|
||||
return $this->url ?? $this->url = $this->kirby()->component('file::url')($this->kirby(), $this, []);
|
||||
}
|
||||
}
|
||||
256
kirby/src/Cms/FileActions.php
Executable file
256
kirby/src/Cms/FileActions.php
Executable file
@@ -0,0 +1,256 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
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): self
|
||||
{
|
||||
if ($sanitize === true) {
|
||||
$name = F::safeName($name);
|
||||
}
|
||||
|
||||
// don't rename if not necessary
|
||||
if ($name === $this->name()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->commit('changeName', [$this, $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 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 integer $sort
|
||||
* @return self
|
||||
*/
|
||||
public function changeSort(int $sort)
|
||||
{
|
||||
return $this->commit('changeSort', [$this, $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();
|
||||
|
||||
$this->rules()->$action(...$arguments);
|
||||
$kirby->trigger('file.' . $action . ':before', ...$arguments);
|
||||
$result = $callback(...$arguments);
|
||||
$kirby->trigger('file.' . $action . ':after', $result, $old);
|
||||
$kirby->cache('pages')->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): self
|
||||
{
|
||||
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']));
|
||||
|
||||
// create the basic file and a test upload object
|
||||
$file = new static($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', [$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', [$this], function ($file) {
|
||||
$file->unpublish();
|
||||
|
||||
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(): self
|
||||
{
|
||||
Media::publish($this->root(), $this->mediaRoot());
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for changeName
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $sanitize
|
||||
* @return self
|
||||
*/
|
||||
public function rename(string $name, bool $sanitize = true)
|
||||
{
|
||||
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): self
|
||||
{
|
||||
return $this->commit('replace', [$this, 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(): self
|
||||
{
|
||||
Media::unpublish($this->parent()->mediaRoot(), $this->filename());
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
65
kirby/src/Cms/FileBlueprint.php
Executable file
65
kirby/src/Cms/FileBlueprint.php
Executable file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of the basic blueprint class
|
||||
* to handle all blueprints for files.
|
||||
*/
|
||||
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,
|
||||
'replace' => null,
|
||||
'update' => null,
|
||||
]
|
||||
);
|
||||
|
||||
// normalize the accept settings
|
||||
$this->props['accept'] = $this->normalizeAccept($this->props['accept'] ?? []);
|
||||
}
|
||||
|
||||
public function accept(): array
|
||||
{
|
||||
return $this->props['accept'];
|
||||
}
|
||||
|
||||
protected function normalizeAccept($accept = null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
230
kirby/src/Cms/FileFoundation.php
Executable file
230
kirby/src/Cms/FileFoundation.php
Executable file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Properties;
|
||||
|
||||
/**
|
||||
* Foundation for all file objects
|
||||
*/
|
||||
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 Asset object
|
||||
*^
|
||||
* @return Image
|
||||
*/
|
||||
public function asset(): Image
|
||||
{
|
||||
return $this->asset = $this->asset ?? new Image($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file exists on disk
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
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 boolean
|
||||
*/
|
||||
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 boolean
|
||||
*/
|
||||
public function isViewable(): bool
|
||||
{
|
||||
$viewable = [
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'gif',
|
||||
'png',
|
||||
'svg',
|
||||
'webp'
|
||||
];
|
||||
|
||||
return in_array($this->extension(), $viewable) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paren app instance
|
||||
*
|
||||
* @return App
|
||||
*/
|
||||
public function kirby(): App
|
||||
{
|
||||
return App::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
{
|
||||
return F::type($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute url for the file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function url(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
}
|
||||
8
kirby/src/Cms/FilePermissions.php
Executable file
8
kirby/src/Cms/FilePermissions.php
Executable file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
class FilePermissions extends ModelPermissions
|
||||
{
|
||||
protected $category = 'files';
|
||||
}
|
||||
192
kirby/src/Cms/FileRules.php
Executable file
192
kirby/src/Cms/FileRules.php
Executable file
@@ -0,0 +1,192 @@
|
||||
<?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
|
||||
*/
|
||||
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('name', $name);
|
||||
|
||||
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 LogicException('The file cannot be deleted');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function replace(File $file, Image $upload): bool
|
||||
{
|
||||
if ($file->permissions()->replace() !== true) {
|
||||
throw new LogicException('The file cannot be replaced');
|
||||
}
|
||||
|
||||
static::validMime($file, $upload->mime());
|
||||
|
||||
|
||||
if ((string)$upload->mime() !== (string)$file->mime()) {
|
||||
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 LogicException('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']
|
||||
]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
81
kirby/src/Cms/FileVersion.php
Executable file
81
kirby/src/Cms/FileVersion.php
Executable file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
class FileVersion extends Asset
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// content fields
|
||||
return $this->original()->content()->get($method, $arguments);
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return dirname($this->original()->id()) . '/' . $this->filename();
|
||||
}
|
||||
|
||||
public function kirby(): App
|
||||
{
|
||||
return $this->original()->kirby();
|
||||
}
|
||||
|
||||
public function modifications(): array
|
||||
{
|
||||
return $this->modifications ?? [];
|
||||
}
|
||||
|
||||
public function original(): File
|
||||
{
|
||||
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(File $original)
|
||||
{
|
||||
$this->original = $original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = array_merge(parent::toArray(), [
|
||||
'modifications' => $this->modifications(),
|
||||
]);
|
||||
|
||||
ksort($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
303
kirby/src/Cms/Filename.php
Executable file
303
kirby/src/Cms/Filename.php
Executable 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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
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 intval($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 intval($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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
132
kirby/src/Cms/Files.php
Executable file
132
kirby/src/Cms/Files.php
Executable file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* An extended version of the Collection
|
||||
* class, that has custom find methods and
|
||||
* a Files::factory method to convert an array
|
||||
* into a Files collection.
|
||||
*/
|
||||
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 $item
|
||||
* @return Files
|
||||
*/
|
||||
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, File::class) === true) {
|
||||
$this->__set($object->id(), $object);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort all given files by the
|
||||
* order in the array
|
||||
*
|
||||
* @param array $files
|
||||
* @return self
|
||||
*/
|
||||
public function changeSort(array $files)
|
||||
{
|
||||
$index = 0;
|
||||
|
||||
foreach ($files as $filename) {
|
||||
if ($file = $this->get($filename)) {
|
||||
$index++;
|
||||
$file->changeSort($index);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a files collection from an array of props
|
||||
*
|
||||
* @param array $files
|
||||
* @param Model $parent
|
||||
* @param array $inject
|
||||
* @return Files
|
||||
*/
|
||||
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 = new File($props);
|
||||
|
||||
$collection->data[$file->id()] = $file;
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a file by id/filename
|
||||
*
|
||||
* @param string $id
|
||||
* @return File|null
|
||||
*/
|
||||
public function findById($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 File|null
|
||||
*/
|
||||
public function findByKey($key)
|
||||
{
|
||||
return $this->findById($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all files by the given template
|
||||
*
|
||||
* @param null|string|array $template
|
||||
* @return self
|
||||
*/
|
||||
public function template($template): self
|
||||
{
|
||||
if (empty($template) === true) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->filterBy('template', is_array($template) ? 'in' : '==', $template);
|
||||
}
|
||||
}
|
||||
67
kirby/src/Cms/Form.php
Executable file
67
kirby/src/Cms/Form.php
Executable file
@@ -0,0 +1,67 @@
|
||||
<?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.
|
||||
*/
|
||||
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'] ?? [];
|
||||
$isDefaultLanguage = $kirby->language()->isDefault();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static function for(Model $model, array $props = [])
|
||||
{
|
||||
// get the original model data
|
||||
$original = $model->content()->toArray();
|
||||
|
||||
// set a few defaults
|
||||
$props['values'] = array_merge($original, $props['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);
|
||||
}
|
||||
}
|
||||
252
kirby/src/Cms/HasChildren.php
Executable file
252
kirby/src/Cms/HasChildren.php
Executable file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
trait HasChildren
|
||||
{
|
||||
|
||||
/**
|
||||
* The Pages collection
|
||||
*
|
||||
* @var Pages
|
||||
*/
|
||||
public $children;
|
||||
|
||||
/**
|
||||
* The list of available drafts
|
||||
*
|
||||
* @var Pages
|
||||
*/
|
||||
public $drafts;
|
||||
|
||||
/**
|
||||
* Returns the Pages collection
|
||||
*
|
||||
* @return 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 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 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 for the site
|
||||
*
|
||||
* @return Pages
|
||||
*/
|
||||
public function drafts(): Pages
|
||||
{
|
||||
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 Pages
|
||||
*/
|
||||
public function find(...$arguments)
|
||||
{
|
||||
return $this->children()->find(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single page or draft
|
||||
*
|
||||
* @return 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 Pages
|
||||
*/
|
||||
public function grandChildren(): Pages
|
||||
{
|
||||
return $this->children()->children();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model has any children
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasChildren(): bool
|
||||
{
|
||||
return $this->children()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model has any drafts
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasDrafts(): bool
|
||||
{
|
||||
return $this->drafts()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated! Use Page::hasUnlistedChildren
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasInvisibleChildren(): bool
|
||||
{
|
||||
return $this->children()->invisible()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page has any listed children
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasListedChildren(): bool
|
||||
{
|
||||
return $this->children()->listed()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page has any unlisted children
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasUnlistedChildren(): bool
|
||||
{
|
||||
return $this->children()->unlisted()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated! Use Page::hasListedChildren
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasVisibleChildren(): bool
|
||||
{
|
||||
return $this->children()->listed()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a flat child index
|
||||
*
|
||||
* @param bool $drafts
|
||||
* @return Pages
|
||||
*/
|
||||
public function index(bool $drafts = false): Pages
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
220
kirby/src/Cms/HasFiles.php
Executable file
220
kirby/src/Cms/HasFiles.php
Executable file
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
trait HasFiles
|
||||
{
|
||||
|
||||
/**
|
||||
* The Files collection
|
||||
*
|
||||
* @var Files
|
||||
*/
|
||||
protected $files;
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type audio
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
public function audio(): Files
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'audio');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type code
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
public function code(): Files
|
||||
{
|
||||
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 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 Files
|
||||
*/
|
||||
public function documents(): Files
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'document');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific file by filename or the first one
|
||||
*
|
||||
* @param string $filename
|
||||
* @param string $in
|
||||
* @return File
|
||||
*/
|
||||
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 Files
|
||||
*/
|
||||
public function files(): 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 File
|
||||
*/
|
||||
public function image(string $filename = null)
|
||||
{
|
||||
return $this->file($filename, 'images');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type image
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
public function images(): Files
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'image');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Files collection
|
||||
*
|
||||
* @param Files|null $files
|
||||
* @return self
|
||||
*/
|
||||
protected function setFiles(array $files = null): self
|
||||
{
|
||||
if ($files !== null) {
|
||||
$this->files = Files::factory($files, $this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Files collection by type videos
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
public function videos(): Files
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'video');
|
||||
}
|
||||
}
|
||||
38
kirby/src/Cms/HasMethods.php
Executable file
38
kirby/src/Cms/HasMethods.php
Executable file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
trait HasMethods
|
||||
{
|
||||
|
||||
/**
|
||||
* All registered methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $methods = [];
|
||||
|
||||
/**
|
||||
* Calls a registered method class with the
|
||||
* passed arguments
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public function callMethod(string $method, array $args = [])
|
||||
{
|
||||
return static::$methods[$method]->call($this, ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object has a registered method
|
||||
*
|
||||
* @param string $method
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasMethod(string $method): bool
|
||||
{
|
||||
return isset(static::$methods[$method]) === true;
|
||||
}
|
||||
}
|
||||
133
kirby/src/Cms/HasSiblings.php
Executable file
133
kirby/src/Cms/HasSiblings.php
Executable file
@@ -0,0 +1,133 @@
|
||||
<?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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
trait HasSiblings
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the position / index in the collection
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function indexOf(): int
|
||||
{
|
||||
return $this->siblingsCollection()->indexOf($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next item in the collection if available
|
||||
*
|
||||
* @return Model|null
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
return $this->siblingsCollection()->nth($this->indexOf() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the end of the collection starting after the current item
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function nextAll()
|
||||
{
|
||||
return $this->siblingsCollection()->slice($this->indexOf() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous item in the collection if available
|
||||
*
|
||||
* @return Model|null
|
||||
*/
|
||||
public function prev()
|
||||
{
|
||||
return $this->siblingsCollection()->nth($this->indexOf() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the beginning of the collection before the current item
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public function prevAll()
|
||||
{
|
||||
return $this->siblingsCollection()->slice(0, $this->indexOf());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all sibling elements
|
||||
*
|
||||
* @param bool $self
|
||||
* @return 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
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNext(): bool
|
||||
{
|
||||
return $this->next() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a previous item in the collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrev(): bool
|
||||
{
|
||||
return $this->prev() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item is the first in the collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFirst(): bool
|
||||
{
|
||||
return $this->siblingsCollection()->first()->is($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item is the last in the collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLast(): bool
|
||||
{
|
||||
return $this->siblingsCollection()->last()->is($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item is at a certain position
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNth(int $n): bool
|
||||
{
|
||||
return $this->indexOf() === $n;
|
||||
}
|
||||
}
|
||||
24
kirby/src/Cms/Html.php
Executable file
24
kirby/src/Cms/Html.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Custom extension of the Toolkit Html builder class
|
||||
* that overwrites the Html::a method to include Cms
|
||||
* Url handling.
|
||||
*/
|
||||
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 a(string $href = null, $text = null, array $attr = []): string
|
||||
{
|
||||
return parent::a(Url::to($href), $text, $attr);
|
||||
}
|
||||
}
|
||||
91
kirby/src/Cms/Ingredients.php
Executable file
91
kirby/src/Cms/Ingredients.php
Executable file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* 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`
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param array $ingredients
|
||||
* @return self
|
||||
*/
|
||||
public static function bake(array $ingredients): self
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
55
kirby/src/Cms/KirbyTag.php
Executable file
55
kirby/src/Cms/KirbyTag.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extended KirbyTag class to provide
|
||||
* common helpers for tag objects
|
||||
*/
|
||||
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 File|null
|
||||
*/
|
||||
public function file(string $path): ?File
|
||||
{
|
||||
$parent = $this->parent();
|
||||
|
||||
if (method_exists($parent, 'file') === true && $file = $parent->file($path)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
if (is_a($parent, File::class) === true && $file = $parent->page()->file($path)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return $this->kirby()->file($path, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Kirby instance
|
||||
*
|
||||
* @return App
|
||||
*/
|
||||
public function kirby(): App
|
||||
{
|
||||
return $this->data['kirby'] ?? App::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model
|
||||
*
|
||||
* @return Page|Site|File|User
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->data['parent'];
|
||||
}
|
||||
}
|
||||
55
kirby/src/Cms/KirbyTags.php
Executable file
55
kirby/src/Cms/KirbyTags.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Extension of `Kirby\Text\KirbyTags` that introduces
|
||||
* `kirbytags:before` and `kirbytags:after` hooks
|
||||
*/
|
||||
class KirbyTags extends \Kirby\Text\KirbyTags
|
||||
{
|
||||
|
||||
/**
|
||||
* The KirbyTag rendering class
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $tagClass = KirbyTag::class;
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param array $data
|
||||
* @param array $options
|
||||
* @param array $hooks
|
||||
* @return string
|
||||
*/
|
||||
public static function parse(string $text = null, array $data = [], array $options = [], array $hooks = []): string
|
||||
{
|
||||
$text = static::hooks($hooks['kirbytags:before'] ?? [], $text, $data, $options);
|
||||
$text = parent::parse($text, $data, $options);
|
||||
$text = static::hooks($hooks['kirbytags:after'] ?? [], $text, $data, $options);
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given hooks and returns the
|
||||
* modified text
|
||||
*
|
||||
* @param array $hooks
|
||||
* @param string $text
|
||||
* @param array $data
|
||||
* @param array $options
|
||||
* @return string
|
||||
*/
|
||||
protected static function hooks(array $hooks, string $text = null, array $data, array $options)
|
||||
{
|
||||
foreach ($hooks as $hook) {
|
||||
$text = $hook->call($data['kirby'], $text, $data, $options);
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
485
kirby/src/Cms/Language.php
Executable file
485
kirby/src/Cms/Language.php
Executable file
@@ -0,0 +1,485 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Represents a content language
|
||||
* in a multi-language setup
|
||||
*/
|
||||
class Language extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $default;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $direction;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $locale;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @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',
|
||||
'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 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 boolean
|
||||
*/
|
||||
protected static function converter(string $from, string $to): bool
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$site = $kirby->site();
|
||||
|
||||
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
|
||||
*
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public static function create(array $props): self
|
||||
{
|
||||
$props['code'] = Str::slug($props['code'] ?? null);
|
||||
$kirby = App::instance();
|
||||
$languages = $kirby->languages();
|
||||
$site = $kirby->site();
|
||||
|
||||
// make the first language the default language
|
||||
if ($languages->count() === 0) {
|
||||
$props['default'] = true;
|
||||
}
|
||||
|
||||
$language = new static($props);
|
||||
|
||||
if ($language->exists() === true) {
|
||||
throw new DuplicateException('The language already exists');
|
||||
}
|
||||
|
||||
$language->save();
|
||||
|
||||
if ($languages->count() === 0) {
|
||||
static::converter('', $language->code());
|
||||
}
|
||||
|
||||
return $language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current language and
|
||||
* all its translation files
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
if ($this->exists() === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$kirby = App::instance();
|
||||
$languages = $kirby->languages();
|
||||
$site = $kirby->site();
|
||||
$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.
|
||||
*
|
||||
* @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 boolean
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->root());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is the default language
|
||||
* for the site.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
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 string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function locale(): string
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable name
|
||||
* of the language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the routing pattern for the language
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function pattern(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the language file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
return App::instance()->root('languages') . '/' . $this->code() . '.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the language settings in the languages folder
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function save(): self
|
||||
{
|
||||
$data = $this->toArray();
|
||||
|
||||
unset($data['url']);
|
||||
|
||||
$export = '<?php' . PHP_EOL . PHP_EOL . 'return ' . var_export($data, true) . ';';
|
||||
|
||||
F::write($this->root(), $export);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $code
|
||||
* @return self
|
||||
*/
|
||||
protected function setCode(string $code): self
|
||||
{
|
||||
$this->code = $code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param boolean $default
|
||||
* @return self
|
||||
*/
|
||||
protected function setDefault(bool $default = false): self
|
||||
{
|
||||
$this->default = $default;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $direction
|
||||
* @return self
|
||||
*/
|
||||
protected function setDirection(string $direction = 'ltr'): self
|
||||
{
|
||||
$this->direction = $direction === 'rtl' ? 'rtl' : 'ltr';
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $locale
|
||||
* @return self
|
||||
*/
|
||||
protected function setLocale(string $locale = null): self
|
||||
{
|
||||
$this->locale = $locale ?? $this->code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
protected function setName(string $name = null): self
|
||||
{
|
||||
$this->name = $name ?? $this->code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $translations
|
||||
* @return self
|
||||
*/
|
||||
protected function setTranslations(array $translations = null): self
|
||||
{
|
||||
$this->translations = $translations ?? [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return self
|
||||
*/
|
||||
protected function setUrl(string $url = null): self
|
||||
{
|
||||
$this->url = $url !== null ? trim($url, '/') : $this->code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(),
|
||||
'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
|
||||
{
|
||||
return Url::to($this->url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update language properties and save them
|
||||
*
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public function update(array $props = null): self
|
||||
{
|
||||
$props['slug'] = Str::slug($props['slug'] ?? null);
|
||||
$kirby = App::instance();
|
||||
$updated = $this->clone($props);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
83
kirby/src/Cms/Languages.php
Executable file
83
kirby/src/Cms/Languages.php
Executable file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* A collection of all defined site languages
|
||||
*/
|
||||
class Languages extends Collection
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns all language codes as array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function codes(): array
|
||||
{
|
||||
return $this->keys();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new language with the given props
|
||||
*
|
||||
* @param array $props
|
||||
* @return Language
|
||||
*/
|
||||
public function create(array $props): Language
|
||||
{
|
||||
return Language::create($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default language
|
||||
*
|
||||
* @return Language|null
|
||||
*/
|
||||
public function default(): ?Language
|
||||
{
|
||||
if ($language = $this->findBy('isDefault', true)) {
|
||||
return $language;
|
||||
} else {
|
||||
return $this->first();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated version of static::default();
|
||||
*
|
||||
* @return Language|null
|
||||
*/
|
||||
public function findDefault(): ?Language
|
||||
{
|
||||
return $this->default();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all defined languages to a collection
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function load(): self
|
||||
{
|
||||
$languages = new static;
|
||||
$files = glob(App::instance()->root('languages') . '/*.php');
|
||||
|
||||
foreach ($files as $file) {
|
||||
$props = include_once $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);
|
||||
|
||||
$language = new Language($props);
|
||||
$languages->data[$language->code()] = $language;
|
||||
}
|
||||
}
|
||||
|
||||
return $languages;
|
||||
}
|
||||
}
|
||||
136
kirby/src/Cms/Media.php
Executable file
136
kirby/src/Cms/Media.php
Executable file
@@ -0,0 +1,136 @@
|
||||
<?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
|
||||
*/
|
||||
class Media
|
||||
{
|
||||
|
||||
/**
|
||||
* Tries to find a job file for the
|
||||
* given filename and then calls the thumb
|
||||
* component to create a thumbnail accordingly
|
||||
*
|
||||
* @param Model $model
|
||||
* @param string $hash
|
||||
* @param string $filename
|
||||
* @return Response|false
|
||||
*/
|
||||
public static function thumb(Model $model, string $hash, string $filename)
|
||||
{
|
||||
try {
|
||||
$kirby = $model->kirby();
|
||||
$url = $model->mediaUrl() . '/' . $hash . '/' . $filename;
|
||||
$root = $model->mediaRoot() . '/' . $hash;
|
||||
$thumb = $root . '/' . $filename;
|
||||
$job = $root . '/.jobs/' . $filename . '.json';
|
||||
$options = Data::read($job);
|
||||
$file = $model->file($options['filename']);
|
||||
|
||||
if (!$file || empty($options) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$kirby->thumb($file->root(), $thumb, $options);
|
||||
|
||||
F::remove($job);
|
||||
|
||||
return Response::file($thumb);
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a file by model and filename
|
||||
* and to copy it to the media folder.
|
||||
*
|
||||
* @param Model $model
|
||||
* @param string $hash
|
||||
* @param string $filename
|
||||
* @return 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)) {
|
||||
|
||||
// the media hash is outdated. redirect to the correct url
|
||||
if ($file->mediaHash() !== $hash) {
|
||||
return Response::redirect($file->mediaUrl(), 307);
|
||||
}
|
||||
|
||||
// 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 string $src
|
||||
* @param string $dest
|
||||
* @return boolean
|
||||
*/
|
||||
public static function publish(string $src, string $dest): bool
|
||||
{
|
||||
$filename = basename($src);
|
||||
$version = dirname($dest);
|
||||
$directory = dirname($version);
|
||||
|
||||
// unpublish all files except stuff in the version folder
|
||||
Media::unpublish($directory, $filename, $version);
|
||||
|
||||
// copy/overwrite the file to the dest folder
|
||||
return F::copy($src, $dest, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all versions of the given filename
|
||||
* within the parent directory
|
||||
*
|
||||
* @param string $directory
|
||||
* @param string $filename
|
||||
* @param string $ignore
|
||||
* @return bool
|
||||
*/
|
||||
public static function unpublish(string $directory, string $filename, string $ignore = null): bool
|
||||
{
|
||||
if (is_dir($directory) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$versions = glob($directory . '/' . crc32($filename) . '*', GLOB_ONLYDIR);
|
||||
|
||||
// delete all versions of the file
|
||||
foreach ($versions as $version) {
|
||||
if ($version === $ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Dir::remove($version);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
105
kirby/src/Cms/Model.php
Executable file
105
kirby/src/Cms/Model.php
Executable file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use stdClass;
|
||||
use ReflectionMethod;
|
||||
use Kirby\Toolkit\Properties;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Foundation for Page, Site, File and User models.
|
||||
*/
|
||||
abstract class Model
|
||||
{
|
||||
use Properties;
|
||||
|
||||
/**
|
||||
* The parent Kirby instance
|
||||
*
|
||||
* @var App
|
||||
*/
|
||||
public static $kirby;
|
||||
|
||||
/**
|
||||
* The parent Site instance
|
||||
*
|
||||
* @var Site
|
||||
*/
|
||||
protected $site;
|
||||
|
||||
/**
|
||||
* Makes it possible to convert the entire model
|
||||
* to a string. Mostly useful for debugging
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Each model must return a unique id
|
||||
*
|
||||
* @return string|int
|
||||
*/
|
||||
public function id()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Kirby instance
|
||||
*
|
||||
* @return App|null
|
||||
*/
|
||||
public function kirby(): App
|
||||
{
|
||||
return static::$kirby = static::$kirby ?? App::instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Site instance
|
||||
*
|
||||
* @return Site|null
|
||||
*/
|
||||
public function site()
|
||||
{
|
||||
return $this->site = $this->site ?? $this->kirby()->site();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the parent Kirby object
|
||||
*
|
||||
* @param Kirby|null $kirby
|
||||
* @return self
|
||||
*/
|
||||
protected function setKirby(App $kirby = null)
|
||||
{
|
||||
static::$kirby = $kirby;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the parent Site object
|
||||
*
|
||||
* @param 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();
|
||||
}
|
||||
}
|
||||
68
kirby/src/Cms/ModelPermissions.php
Executable file
68
kirby/src/Cms/ModelPermissions.php
Executable file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
abstract class ModelPermissions
|
||||
{
|
||||
protected $category;
|
||||
protected $model;
|
||||
protected $options;
|
||||
protected $permissions;
|
||||
protected $user;
|
||||
|
||||
public function __call(string $method, array $arguments = [])
|
||||
{
|
||||
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
|
||||
{
|
||||
if ($this->user->role()->id() === 'nobody') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (method_exists($this, 'can' . $action) === true && $this->{'can' . $action}() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($this->options[$action]) === true && $this->options[$action] === false) {
|
||||
return 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;
|
||||
}
|
||||
}
|
||||
442
kirby/src/Cms/ModelWithContent.php
Executable file
442
kirby/src/Cms/ModelWithContent.php
Executable file
@@ -0,0 +1,442 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
abstract class ModelWithContent extends Model
|
||||
{
|
||||
|
||||
/**
|
||||
* The content
|
||||
*
|
||||
* @var Content
|
||||
*/
|
||||
public $content;
|
||||
|
||||
/**
|
||||
* @var Translations
|
||||
*/
|
||||
public $translations;
|
||||
|
||||
/**
|
||||
* Returns the blueprint of the model
|
||||
*
|
||||
* @return 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 Content
|
||||
*/
|
||||
public function content(string $languageCode = null): Content
|
||||
{
|
||||
|
||||
// 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
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the content that should be written
|
||||
* to the text file
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function contentFileDirectory(): ?string
|
||||
{
|
||||
return $this->root();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension of the content file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileExtension(): string
|
||||
{
|
||||
return $this->kirby()->contentExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Needs to be declared by the final model
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function contentFileName(): string;
|
||||
|
||||
/**
|
||||
* Decrement a given field value
|
||||
*
|
||||
* @param string $field
|
||||
* @param integer $by
|
||||
* @param integer $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 integer $by
|
||||
* @param integer $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 data has any errors
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return Form::for($this)->hasErrors() === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the content from the content file
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
abstract public function root(): ?string;
|
||||
|
||||
/**
|
||||
* Stores the content on disk
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
// merge the translation with the new data
|
||||
$translation->update($data, $overwrite);
|
||||
|
||||
// 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 Content|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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single translation by language code
|
||||
* If no code is specified the current translation is returned
|
||||
*
|
||||
* @param string $languageCode
|
||||
* @return Translation|null
|
||||
*/
|
||||
public function translation(string $languageCode = null)
|
||||
{
|
||||
return $this->translations()->find($languageCode ?? $this->kirby()->language()->code());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the translations collection
|
||||
*
|
||||
* @return 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 $language
|
||||
* @param boolean $validate
|
||||
* @return self
|
||||
*/
|
||||
public function update(array $input = null, string $languageCode = null, bool $validate = false)
|
||||
{
|
||||
$form = Form::for($this, [
|
||||
'input' => $input,
|
||||
'ignoreDisabled' => $validate === false,
|
||||
]);
|
||||
|
||||
// validate the input
|
||||
if ($validate === true) {
|
||||
if ($form->isInvalid() === true) {
|
||||
throw new InvalidArgumentException([
|
||||
'fallback' => 'Invalid form with errors',
|
||||
'details' => $form->errors()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->commit('update', [$this, $form->data(), $form->strings(), $languageCode], function ($model, $values, $strings, $languageCode) {
|
||||
return $model->save($strings, $languageCode, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level data writer method
|
||||
* to store the given data on disk or anywhere else
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $languageCode
|
||||
* @return boolean
|
||||
*/
|
||||
public function writeContent(array $data, string $languageCode = null): bool
|
||||
{
|
||||
return Data::write(
|
||||
$this->contentFile($languageCode),
|
||||
$this->contentFileData($data, $languageCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
39
kirby/src/Cms/Nest.php
Executable file
39
kirby/src/Cms/Nest.php
Executable file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Obj;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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_string($value) === true) {
|
||||
$result[$key] = new Field($parent, $key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_int(key($data))) {
|
||||
return new NestCollection($result);
|
||||
} else {
|
||||
return new NestObject($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
kirby/src/Cms/NestCollection.php
Executable file
25
kirby/src/Cms/NestCollection.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Toolkit\Collection as BaseCollection;
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
35
kirby/src/Cms/NestObject.php
Executable file
35
kirby/src/Cms/NestObject.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Obj;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
1510
kirby/src/Cms/Page.php
Executable file
1510
kirby/src/Cms/Page.php
Executable file
File diff suppressed because it is too large
Load Diff
730
kirby/src/Cms/PageActions.php
Executable file
730
kirby/src/Cms/PageActions.php
Executable file
@@ -0,0 +1,730 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Data\Data;
|
||||
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;
|
||||
|
||||
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): self
|
||||
{
|
||||
if ($this->isDraft() === true) {
|
||||
throw new LogicException('Drafts cannot change their sorting number');
|
||||
}
|
||||
|
||||
return $this->commit('changeNum', [$this, $num], function ($oldPage, $num) {
|
||||
$newPage = $oldPage->clone([
|
||||
'num' => $num,
|
||||
'dirname' => null,
|
||||
'root' => null
|
||||
]);
|
||||
|
||||
// actually move the page on disk
|
||||
if ($oldPage->exists() === true) {
|
||||
Dir::move($oldPage->root(), $newPage->root());
|
||||
}
|
||||
|
||||
// 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 $language
|
||||
* @return self
|
||||
*/
|
||||
public function changeSlug(string $slug, string $languageCode = null): self
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
return $this->commit('changeSlug', [$this, $slug, $languageCode = null], function ($oldPage, $slug) {
|
||||
$newPage = $oldPage->clone([
|
||||
'slug' => $slug,
|
||||
'dirname' => null,
|
||||
'root' => null
|
||||
]);
|
||||
|
||||
// actually move stuff on disk
|
||||
if ($oldPage->exists() === true) {
|
||||
if (Dir::move($oldPage->root(), $newPage->root()) !== true) {
|
||||
throw new LogicException('The page directory cannot be moved');
|
||||
}
|
||||
|
||||
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 $language
|
||||
* @return self
|
||||
*/
|
||||
protected function changeSlugForLanguage(string $slug, string $languageCode = null): self
|
||||
{
|
||||
$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');
|
||||
}
|
||||
|
||||
return $this->commit('changeSlug', [$this, $slug, $languageCode], 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 integer $position Optional sorting number
|
||||
* @return Page
|
||||
*/
|
||||
public function changeStatus(string $status, int $position = null): self
|
||||
{
|
||||
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(): self
|
||||
{
|
||||
$page = $this->commit('changeStatus', [$this, 'draft'], function ($page) {
|
||||
return $page->unpublish();
|
||||
});
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
protected function changeStatusToListed(int $position = null): self
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
$page = $this->commit('changeStatus', [$this, 'listed', $num], function ($page, $status, $position) {
|
||||
return $page->publish()->changeNum($position);
|
||||
});
|
||||
|
||||
if ($this->blueprint()->num() === 'default') {
|
||||
$page->resortSiblingsAfterListing($num);
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
protected function changeStatusToUnlisted(): self
|
||||
{
|
||||
if ($this->status() === 'unlisted') {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$page = $this->commit('changeStatus', [$this, 'unlisted'], 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): self
|
||||
{
|
||||
if ($template === $this->template()->name()) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// prepare data to transfer between blueprints
|
||||
$oldBlueprint = 'pages/' . $this->template();
|
||||
$newBlueprint = 'pages/' . $template;
|
||||
|
||||
return $this->commit('changeTemplate', [$this, $template], function ($oldPage, $template) use ($oldBlueprint, $newBlueprint) {
|
||||
if ($this->kirby()->multilang() === true) {
|
||||
$newPage = $this->clone([
|
||||
'template' => $template
|
||||
]);
|
||||
|
||||
foreach ($this->kirby()->languages()->codes() as $code) {
|
||||
if (F::remove($oldPage->contentFile($code)) !== true) {
|
||||
throw new LogicException('The old text file could not be removed');
|
||||
}
|
||||
|
||||
// convert the content to the new blueprint
|
||||
$content = $oldPage->transferData($oldPage->content($code), $oldBlueprint, $newBlueprint)['data'];
|
||||
|
||||
// save the language file
|
||||
$newPage->save($content, $code);
|
||||
}
|
||||
|
||||
// return a fresh copy of the object
|
||||
return $newPage->clone();
|
||||
} else {
|
||||
$newPage = $this->clone([
|
||||
'content' => $this->transferData($this->content(), $oldBlueprint, $newBlueprint)['data'],
|
||||
'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): self
|
||||
{
|
||||
return $this->commit('changeTitle', [$this, $title, $languageCode], 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 mixed ...$arguments
|
||||
* @return mixed
|
||||
*/
|
||||
protected function commit(string $action, array $arguments, Closure $callback)
|
||||
{
|
||||
$old = $this->hardcopy();
|
||||
|
||||
$this->rules()->$action(...$arguments);
|
||||
$this->kirby()->trigger('page.' . $action . ':before', ...$arguments);
|
||||
$result = $callback(...$arguments);
|
||||
$this->kirby()->trigger('page.' . $action . ':after', $result, $old);
|
||||
$this->kirby()->cache('pages')->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and stores a new page
|
||||
*
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public static function create(array $props): self
|
||||
{
|
||||
// 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, $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): self
|
||||
{
|
||||
$props = array_merge($props, [
|
||||
'url' => null,
|
||||
'num' => null,
|
||||
'parent' => $this,
|
||||
'site' => $this->site(),
|
||||
]);
|
||||
|
||||
return static::create($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the sorting number for the page
|
||||
* depending on the blueprint settings
|
||||
*
|
||||
* @param integer $num
|
||||
* @return integer
|
||||
*/
|
||||
public function createNum(int $num = null): int
|
||||
{
|
||||
$mode = $this->blueprint()->num();
|
||||
|
||||
switch ($mode) {
|
||||
case 'zero':
|
||||
return 0;
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
$format = 'date' ? 'Ymd' : 'YmdHi';
|
||||
$date = $this->content()->get('date')->value();
|
||||
$time = empty($date) === true ? time() : strtotime($date);
|
||||
|
||||
return date($format, $time);
|
||||
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:
|
||||
$template = Str::template($mode, [
|
||||
'kirby' => $this->kirby(),
|
||||
'page' => $this,
|
||||
'site' => $this->site(),
|
||||
]);
|
||||
|
||||
return intval($template);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the page
|
||||
*
|
||||
* @param bool $force
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(bool $force = false): bool
|
||||
{
|
||||
return $this->commit('delete', [$this, $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;
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public function purge(): self
|
||||
{
|
||||
$this->children = null;
|
||||
$this->blueprint = null;
|
||||
$this->drafts = null;
|
||||
$this->files = null;
|
||||
$this->content = null;
|
||||
$this->inventory = 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', 'desc');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function sort($position = null)
|
||||
{
|
||||
return $this->changeStatus('listed', $position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers data from old to new blueprint and tracks changes
|
||||
*
|
||||
* @param Content $content
|
||||
* @param string $old Old blueprint
|
||||
* @param string $new New blueprint
|
||||
* @return array
|
||||
*/
|
||||
protected function transferData(Content $content, string $old, string $new): array
|
||||
{
|
||||
// Prepare data
|
||||
$data = [];
|
||||
$old = Blueprint::factory($old, 'pages/default', $this);
|
||||
$new = Blueprint::factory($new, 'pages/default', $this);
|
||||
$oldForm = new Form(['fields' => $old->fields(), 'model' => $this]);
|
||||
$newForm = new Form(['fields' => $new->fields(), 'model' => $this]);
|
||||
$oldFields = $oldForm->fields();
|
||||
$newFields = $newForm->fields();
|
||||
|
||||
// Tracking changes
|
||||
$added = [];
|
||||
$replaced = [];
|
||||
$removed = [];
|
||||
|
||||
// Ensure to keep title
|
||||
$data['title'] = $content->get('title')->value();
|
||||
|
||||
// Go through all fields of new template
|
||||
foreach ($newFields as $newField) {
|
||||
$name = $newField->name();
|
||||
$oldField = $oldFields->get($name);
|
||||
|
||||
// Field name matches with old template
|
||||
if ($oldField !== null) {
|
||||
|
||||
// Same field type, add and keep value
|
||||
if ($oldField->type() === $newField->type()) {
|
||||
$data[$name] = $content->get($name)->value();
|
||||
|
||||
// Different field type, add with empty value
|
||||
} else {
|
||||
$data[$name] = null;
|
||||
$replaced[$name] = $oldFields->get($name)->label() ?? $name;
|
||||
}
|
||||
|
||||
// Field does not exist in old template,
|
||||
// add with empty or preserved value
|
||||
} else {
|
||||
$preserved = $content->get($name);
|
||||
$data[$name] = $preserved ? $preserved->value(): null;
|
||||
$added[$name] = $newField->label() ?? $name;
|
||||
}
|
||||
}
|
||||
|
||||
// Go through all values to preserve them
|
||||
foreach ($content->fields() as $field) {
|
||||
$name = $field->key();
|
||||
$newField = $newFields->get($name);
|
||||
|
||||
if ($newField === null) {
|
||||
$data[$name] = $field->value();
|
||||
$removed[$name] = $field->name();
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'added' => $added,
|
||||
'replaced' => $replaced,
|
||||
'removed' => $removed
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 boolean $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;
|
||||
}
|
||||
}
|
||||
196
kirby/src/Cms/PageBlueprint.php
Executable file
196
kirby/src/Cms/PageBlueprint.php
Executable file
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
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,
|
||||
'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',
|
||||
'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;
|
||||
}
|
||||
|
||||
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|boolean
|
||||
*/
|
||||
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'];
|
||||
}
|
||||
}
|
||||
53
kirby/src/Cms/PagePermissions.php
Executable file
53
kirby/src/Cms/PagePermissions.php
Executable file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
241
kirby/src/Cms/PageRules.php
Executable file
241
kirby/src/Cms/PageRules.php
Executable file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Validators for all page actions
|
||||
*/
|
||||
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 ($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']);
|
||||
}
|
||||
|
||||
$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']);
|
||||
}
|
||||
|
||||
if (($page->hasChildren() === true || $page->hasDrafts() === true) && $force === false) {
|
||||
throw new LogicException(['key' => 'page.delete.hasChildren']);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
188
kirby/src/Cms/PageSiblings.php
Executable file
188
kirby/src/Cms/PageSiblings.php
Executable file
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
trait PageSiblings
|
||||
{
|
||||
|
||||
/**
|
||||
* @deprecated Use Page::hasNextUnlisted instead
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasNextInvisible(): bool
|
||||
{
|
||||
return $this->hasNextUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a next listed
|
||||
* page in the siblings collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextListed(): bool
|
||||
{
|
||||
return $this->nextListed() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Page::hasNextListed instead
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasNextVisible(): bool
|
||||
{
|
||||
return $this->hasNextListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a next unlisted
|
||||
* page in the siblings collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextUnlisted(): bool
|
||||
{
|
||||
return $this->nextUnlisted() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Page::hasPrevUnlisted instead
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasPrevInvisible(): bool
|
||||
{
|
||||
return $this->hasPrevUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a previous listed
|
||||
* page in the siblings collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrevListed(): bool
|
||||
{
|
||||
return $this->prevListed() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a previous unlisted
|
||||
* page in the siblings collection
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrevUnlisted(): bool
|
||||
{
|
||||
return $this->prevUnlisted() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Page::hasPrevListed instead
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasPrevVisible(): bool
|
||||
{
|
||||
return $this->hasPrevListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Page::nextUnlisted() instead
|
||||
* @return self|null
|
||||
*/
|
||||
public function nextInvisible()
|
||||
{
|
||||
return $this->nextUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next listed page if it exists
|
||||
*
|
||||
* @return self|null
|
||||
*/
|
||||
public function nextListed()
|
||||
{
|
||||
return $this->nextAll()->listed()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next unlisted page if it exists
|
||||
*
|
||||
* @return self|null
|
||||
*/
|
||||
public function nextUnlisted()
|
||||
{
|
||||
return $this->nextAll()->unlisted()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Page::prevListed() instead
|
||||
* @return self|null
|
||||
*/
|
||||
public function nextVisible()
|
||||
{
|
||||
return $this->nextListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Page::prevUnlisted() instead
|
||||
* @return self|null
|
||||
*/
|
||||
public function prevInvisible()
|
||||
{
|
||||
return $this->prevUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous listed page
|
||||
*
|
||||
* @return self|null
|
||||
*/
|
||||
public function prevListed()
|
||||
{
|
||||
return $this->prevAll()->listed()->last();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous unlisted page
|
||||
*
|
||||
* @return self|null
|
||||
*/
|
||||
public function prevUnlisted()
|
||||
{
|
||||
return $this->prevAll()->unlisted()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use Page::prevListed() instead
|
||||
* @return self|null
|
||||
*/
|
||||
public function prevVisible()
|
||||
{
|
||||
return $this->prevListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Private siblings collector
|
||||
*
|
||||
* @return 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 self
|
||||
*/
|
||||
public function templateSiblings(bool $self = true)
|
||||
{
|
||||
return $this->siblings($self)->filterBy('intendedTemplate', $this->intendedTemplate()->name());
|
||||
}
|
||||
}
|
||||
482
kirby/src/Cms/Pages.php
Executable file
482
kirby/src/Cms/Pages.php
Executable file
@@ -0,0 +1,482 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The Pages collection contains
|
||||
* any number and mixture of page objects
|
||||
* They don't necessarily have to belong
|
||||
* to the same parent unless it is passed
|
||||
* as second argument in the constructor.
|
||||
*
|
||||
* Pages collection can be constructed very
|
||||
* easily:
|
||||
*
|
||||
* ```php
|
||||
* $collection = new Pages([
|
||||
* new Page(['id' => 'project-a']),
|
||||
* new Page(['id' => 'project-b']),
|
||||
* new Page(['id' => 'project-c']),
|
||||
* ]);
|
||||
* ```
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
class Pages extends Collection
|
||||
{
|
||||
|
||||
/**
|
||||
* Cache for the index
|
||||
*
|
||||
* @var null|Pages
|
||||
*/
|
||||
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 $item
|
||||
* @return Pages
|
||||
*/
|
||||
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, Page::class) === true) {
|
||||
$this->__set($object->id(), $object);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all audio files of all children
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
public function audio(): Files
|
||||
{
|
||||
return $this->files()->filterBy("type", "audio");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all children for each page in the array
|
||||
*
|
||||
* @return Pages
|
||||
*/
|
||||
public function children(): Pages
|
||||
{
|
||||
$children = new Pages([], $this->parent);
|
||||
|
||||
foreach ($this->data as $pageKey => $page) {
|
||||
foreach ($page->children() as $childKey => $child) {
|
||||
$children->data[$childKey] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all code files of all children
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
public function code(): Files
|
||||
{
|
||||
return $this->files()->filterBy("type", "code");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all documents of all children
|
||||
*
|
||||
* @return Files
|
||||
*/
|
||||
public function documents(): Files
|
||||
{
|
||||
return $this->files()->filterBy("type", "document");
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all drafts for all pages in the collection
|
||||
*
|
||||
* @return Pages
|
||||
*/
|
||||
public function drafts()
|
||||
{
|
||||
$drafts = new Pages([], $this->parent);
|
||||
|
||||
foreach ($this->data as $pageKey => $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 Model $parent
|
||||
* @param array $inject
|
||||
* @param bool $draft
|
||||
* @return Pages
|
||||
*/
|
||||
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 Files
|
||||
*/
|
||||
public function files(): Files
|
||||
{
|
||||
$files = new Files([], $this->parent);
|
||||
|
||||
foreach ($this->data as $pageKey => $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 $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function findById($id)
|
||||
{
|
||||
$id = trim($id, '/');
|
||||
$page = $this->get($id);
|
||||
$multiLang = App::instance()->multilang();
|
||||
|
||||
if ($multiLang === true) {
|
||||
$page = $this->findBy('slug', $id);
|
||||
}
|
||||
|
||||
if (!$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 $startAt
|
||||
* @return mixed
|
||||
*/
|
||||
public function findByIdRecursive($id, $startAt = null, bool $multiLang = false)
|
||||
{
|
||||
$path = explode('/', $id);
|
||||
$collection = $this;
|
||||
$item = null;
|
||||
$query = $startAt;
|
||||
|
||||
foreach ($path as $key) {
|
||||
$query = ltrim($query . '/' . $key, '/');
|
||||
$item = $collection->get($query) ?? null;
|
||||
|
||||
if ($item === null && $multiLang === true) {
|
||||
$item = $collection->findBy('slug', $key);
|
||||
}
|
||||
|
||||
if ($item === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collection = $item->children();
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the specialized find by id method
|
||||
*
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function findByKey($key)
|
||||
{
|
||||
return $this->findById($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for Pages::findById
|
||||
*
|
||||
* @param string $id
|
||||
* @return Page|null
|
||||
*/
|
||||
public function findByUri(string $id)
|
||||
{
|
||||
return $this->findById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the currently open page
|
||||
*
|
||||
* @return Page|null
|
||||
*/
|
||||
public function findOpen()
|
||||
{
|
||||
return $this->findBy('isOpen', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom getter that is able to find
|
||||
* extension pages
|
||||
*
|
||||
* @param string $key
|
||||
* @return 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 Files
|
||||
*/
|
||||
public function images(): Files
|
||||
{
|
||||
return $this->files()->filterBy("type", "image");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a recursive flat index of all
|
||||
* pages and subpages, etc.
|
||||
*
|
||||
* @param bool $drafts
|
||||
* @return Pages
|
||||
*/
|
||||
public function index(bool $drafts = false): Pages
|
||||
{
|
||||
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;
|
||||
|
||||
foreach ($page->index($drafts) as $childKey => $child) {
|
||||
$this->index->data[$childKey] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated alias for Pages::unlisted()
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function invisible(): self
|
||||
{
|
||||
return $this->filterBy('isUnlisted', '==', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all listed pages in the collection
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function listed(): self
|
||||
{
|
||||
return $this->filterBy('isListed', '==', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all unlisted pages in the collection
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function unlisted(): self
|
||||
{
|
||||
return $this->filterBy('isUnlisted', '==', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Include all given items in the collection
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function merge(...$args): self
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 self
|
||||
*/
|
||||
public function published(): self
|
||||
{
|
||||
return $this->filterBy('isDraft', '==', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all pages by the given template
|
||||
*
|
||||
* @param null|string|array $template
|
||||
* @return self
|
||||
*/
|
||||
public function template($templates): self
|
||||
{
|
||||
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 Files
|
||||
*/
|
||||
public function videos(): Files
|
||||
{
|
||||
return $this->files()->filterBy("type", "video");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated alias for Pages::listed()
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function visible(): self
|
||||
{
|
||||
return $this->filterBy('isListed', '==', true);
|
||||
}
|
||||
}
|
||||
172
kirby/src/Cms/Pagination.php
Executable file
172
kirby/src/Cms/Pagination.php
Executable file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Http\Uri;
|
||||
use Kirby\Toolkit\Pagination as BasePagination;
|
||||
|
||||
/**
|
||||
* The extended Pagination class handles
|
||||
* URLs in addition to the pagination features
|
||||
* from Kirby\Toolkit\Pagination
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
class Pagination extends BasePagination
|
||||
{
|
||||
|
||||
/**
|
||||
* Pagination method (param or query)
|
||||
*
|
||||
* @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'], 1);
|
||||
} else {
|
||||
$params['page'] = $params['page'] ?? $params['url']->params()->get($params['variable'], 1);
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
public function nextPageUrl()
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function pageUrl(int $page = null)
|
||||
{
|
||||
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;
|
||||
} else {
|
||||
$url->params->$variable = $pageValue;
|
||||
}
|
||||
|
||||
return $url->toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Url for the previous page.
|
||||
* Returns null if there's no previous page.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function prevPageUrl()
|
||||
{
|
||||
if ($page = $this->prevPage()) {
|
||||
return $this->pageUrl($page);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
94
kirby/src/Cms/Panel.php
Executable file
94
kirby/src/Cms/Panel.php
Executable file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Http\Response;
|
||||
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`
|
||||
*/
|
||||
class Panel
|
||||
{
|
||||
|
||||
/**
|
||||
* Links all dist files in the media folder
|
||||
* and returns the link to the requested asset
|
||||
*
|
||||
* @param App $kirby
|
||||
* @return bool
|
||||
*/
|
||||
public static function link(App $kirby): bool
|
||||
{
|
||||
$mediaRoot = $kirby->root('media') . '/panel';
|
||||
$panelRoot = $kirby->root('panel') . '/dist';
|
||||
$versionHash = md5($kirby->version());
|
||||
$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($kirby->root('panel') . '/dist', $versionRoot) !== true) {
|
||||
throw new Exception('Panel assets could not be linked');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the main panel view
|
||||
*
|
||||
* @param App $kirby
|
||||
* @return Response
|
||||
*/
|
||||
public static function render(App $kirby): Response
|
||||
{
|
||||
try {
|
||||
if (static::link($kirby) === true) {
|
||||
usleep(1);
|
||||
go($kirby->request()->url());
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
die('The panel assets cannot be installed properly. Please check permissions of your media folder.');
|
||||
}
|
||||
|
||||
$view = new View($kirby->root('kirby') . '/views/panel.php', [
|
||||
'kirby' => $kirby,
|
||||
'config' => $kirby->option('panel'),
|
||||
'assetUrl' => $kirby->url('media') . '/panel/' . md5($kirby->version()),
|
||||
'pluginCss' => $kirby->url('media') . '/plugins/index.css',
|
||||
'pluginJs' => $kirby->url('media') . '/plugins/index.js',
|
||||
'icons' => F::read($kirby->root('panel') . '/dist/img/icons.svg'),
|
||||
'panelUrl' => $url = $kirby->url('panel'),
|
||||
'options' => [
|
||||
'url' => $url,
|
||||
'site' => $kirby->url('index'),
|
||||
'api' => $kirby->url('api'),
|
||||
'csrf' => $kirby->option('api')['csrf'] ?? csrf(),
|
||||
'translation' => 'en',
|
||||
'debug' => true,
|
||||
'search' => [
|
||||
'limit' => $kirby->option('panel')['search']['limit'] ?? 10
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
return new Response($view->render());
|
||||
}
|
||||
}
|
||||
161
kirby/src/Cms/Permissions.php
Executable file
161
kirby/src/Cms/Permissions.php
Executable file
@@ -0,0 +1,161 @@
|
||||
<?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.
|
||||
*/
|
||||
class Permissions
|
||||
{
|
||||
protected $actions = [
|
||||
'access' => [
|
||||
'panel' => true,
|
||||
'users' => true,
|
||||
'site' => true
|
||||
],
|
||||
'files' => [
|
||||
'changeName' => true,
|
||||
'create' => true,
|
||||
'delete' => true,
|
||||
'replace' => true,
|
||||
'update' => true
|
||||
],
|
||||
'languages' => [
|
||||
'create' => true,
|
||||
'delete' => true
|
||||
],
|
||||
'pages' => [
|
||||
'changeSlug' => true,
|
||||
'changeStatus' => true,
|
||||
'changeTemplate' => true,
|
||||
'changeTitle' => true,
|
||||
'create' => true,
|
||||
'delete' => 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 = [])
|
||||
{
|
||||
if (is_bool($settings) === true) {
|
||||
return $this->setAll($settings);
|
||||
}
|
||||
|
||||
if (is_array($settings) === true) {
|
||||
return $this->setCategories($settings);
|
||||
}
|
||||
}
|
||||
|
||||
public function for(string $category = null, string $action = null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return $this->hasCategory($category) === true && array_key_exists($action, $this->actions[$category]) === true;
|
||||
}
|
||||
|
||||
protected function hasCategory(string $category)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
106
kirby/src/Cms/Plugin.php
Executable file
106
kirby/src/Cms/Plugin.php
Executable file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* Represents a Plugin and handles parsing of
|
||||
* the composer.json. It also creates the prefix
|
||||
* and media url for the plugin.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
115
kirby/src/Cms/PluginAssets.php
Executable file
115
kirby/src/Cms/PluginAssets.php
Executable file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
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.
|
||||
*/
|
||||
class PluginAssets
|
||||
{
|
||||
|
||||
/**
|
||||
* Concatenate all plugin js and css files into
|
||||
* a single file and copy them to /media/plugins/index.css or /media/plugins/index.js
|
||||
*
|
||||
* @param string $extension
|
||||
* @return string
|
||||
*/
|
||||
public static function index(string $extension): string
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$cache = $kirby->root('media') . '/plugins/.index.' . $extension;
|
||||
$build = false;
|
||||
$modified = [0];
|
||||
$assets = [];
|
||||
|
||||
foreach ($kirby->plugins() as $plugin) {
|
||||
$file = $plugin->root() . '/index.' . $extension;
|
||||
|
||||
if (file_exists($file) === true) {
|
||||
$assets[] = $file;
|
||||
$modified[] = F::modified($file);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_exists($cache) === false || filemtime($cache) < max($modified)) {
|
||||
$dist = [];
|
||||
foreach ($assets as $asset) {
|
||||
$dist[] = file_get_contents($asset);
|
||||
}
|
||||
$dist = implode(PHP_EOL, $dist);
|
||||
F::write($cache, $dist);
|
||||
} else {
|
||||
$dist = file_get_contents($cache);
|
||||
}
|
||||
|
||||
return $dist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean old/deprecated assets on every resolve
|
||||
*
|
||||
* @param string $pluginName
|
||||
* @return void
|
||||
*/
|
||||
public static function clean(string $pluginName)
|
||||
{
|
||||
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 string
|
||||
*/
|
||||
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;
|
||||
|
||||
F::link($source, $target, 'symlink');
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
20
kirby/src/Cms/R.php
Executable file
20
kirby/src/Cms/R.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Http\Request;
|
||||
use Kirby\Toolkit\Facade;
|
||||
|
||||
/**
|
||||
* Shortcut to the request object
|
||||
*/
|
||||
class R extends Facade
|
||||
{
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
protected static function instance()
|
||||
{
|
||||
return App::instance()->request();
|
||||
}
|
||||
}
|
||||
218
kirby/src/Cms/Responder.php
Executable file
218
kirby/src/Cms/Responder.php
Executable file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Mime;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Global response configuration
|
||||
*/
|
||||
class Responder
|
||||
{
|
||||
|
||||
/**
|
||||
* HTTP status code
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
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 $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 integer $code
|
||||
* @return integer|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
|
||||
* @return self
|
||||
*/
|
||||
public function fromArray(array $response)
|
||||
{
|
||||
$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 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 integer|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 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;
|
||||
}
|
||||
}
|
||||
29
kirby/src/Cms/Response.php
Executable file
29
kirby/src/Cms/Response.php
Executable file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Http\Response as BaseResponse;
|
||||
|
||||
/**
|
||||
* Custom response object with an optimized
|
||||
* redirect method to build correct Urls
|
||||
*/
|
||||
class Response extends BaseResponse
|
||||
{
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
167
kirby/src/Cms/Role.php
Executable file
167
kirby/src/Cms/Role.php
Executable file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* Represents a User role with attached
|
||||
* permissions. Roles are defined by user blueprints.
|
||||
*/
|
||||
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()
|
||||
{
|
||||
return [
|
||||
'admin' => [
|
||||
'description' => 'The admin has all rights',
|
||||
'name' => 'admin',
|
||||
'title' => 'Admin',
|
||||
'permissions' => true,
|
||||
],
|
||||
'nobody' => [
|
||||
'description' => 'This is a fallback role without any permissions',
|
||||
'name' => 'nobody',
|
||||
'title' => 'Nobody',
|
||||
'permissions' => false,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function description()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public static function factory(array $props, array $inject = []): self
|
||||
{
|
||||
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';
|
||||
}
|
||||
|
||||
public static function load(string $file, array $inject = []): self
|
||||
{
|
||||
$name = F::name($file);
|
||||
|
||||
$data = Data::read($file);
|
||||
$data['name'] = $name;
|
||||
|
||||
return static::factory($data, $inject);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public static function nobody(array $inject = [])
|
||||
{
|
||||
try {
|
||||
return static::load('nobody');
|
||||
} catch (Exception $e) {
|
||||
return static::factory(static::defaults()['nobody'], $inject);
|
||||
}
|
||||
}
|
||||
|
||||
public function permissions(): Permissions
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
protected function setDescription(string $description = null): self
|
||||
{
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setPermissions($permissions = null): self
|
||||
{
|
||||
$this->permissions = new Permissions($permissions);
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setTitle($title = null): self
|
||||
{
|
||||
$this->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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
76
kirby/src/Cms/Roles.php
Executable file
76
kirby/src/Cms/Roles.php
Executable file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class Roles extends Collection
|
||||
{
|
||||
public static function factory(array $roles, array $inject = []): self
|
||||
{
|
||||
$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');
|
||||
}
|
||||
|
||||
public static function load(string $root = null, array $inject = []): self
|
||||
{
|
||||
$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 (Dir::read($root) as $filename) {
|
||||
if ($filename === 'default.yml') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$role = Role::load($root . '/' . $filename, $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');
|
||||
}
|
||||
}
|
||||
20
kirby/src/Cms/S.php
Executable file
20
kirby/src/Cms/S.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Session\Session;
|
||||
use Kirby\Toolkit\Facade;
|
||||
|
||||
/**
|
||||
* Shortcut to the session object
|
||||
*/
|
||||
class S extends Facade
|
||||
{
|
||||
/**
|
||||
* @return Session
|
||||
*/
|
||||
protected static function instance()
|
||||
{
|
||||
return App::instance()->session();
|
||||
}
|
||||
}
|
||||
111
kirby/src/Cms/Search.php
Executable file
111
kirby/src/Cms/Search.php
Executable file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Search class extracts the
|
||||
* search logic from collections, to
|
||||
* provide a more globally usable interface
|
||||
* for any searches.
|
||||
*/
|
||||
class Search
|
||||
{
|
||||
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
|
||||
*/
|
||||
public static function collection(Collection $collection, string $query = null, $params = [])
|
||||
{
|
||||
if (empty(trim($query)) === true) {
|
||||
return $collection->limit(0);
|
||||
}
|
||||
|
||||
if (is_string($params) === true) {
|
||||
$params = ['fields' => Str::split($params, '|')];
|
||||
}
|
||||
|
||||
$defaults = [
|
||||
'fields' => [],
|
||||
'minlength' => 2,
|
||||
'score' => [],
|
||||
'words' => false,
|
||||
];
|
||||
|
||||
$options = array_merge($defaults, $params);
|
||||
$collection = clone $collection;
|
||||
$searchwords = preg_replace('/(\s)/u', ',', $query);
|
||||
$searchwords = Str::split($searchwords, ',', $options['minlength']);
|
||||
$lowerQuery = strtolower($query);
|
||||
|
||||
if (empty($options['stopwords']) === false) {
|
||||
$searchwords = array_diff($searchwords, $options['stopwords']);
|
||||
}
|
||||
|
||||
$searchwords = array_map(function ($value) use ($options) {
|
||||
return $options['words'] ? '\b' . preg_quote($value) . '\b' : preg_quote($value);
|
||||
}, $searchwords);
|
||||
|
||||
$preg = '!(' . implode('|', $searchwords) . ')!i';
|
||||
$results = $collection->filter(function ($item) use ($query, $preg, $options, $lowerQuery) {
|
||||
$data = $item->content()->toArray();
|
||||
$keys = array_keys($data);
|
||||
$keys[] = 'id';
|
||||
|
||||
if (empty($options['fields']) === false) {
|
||||
$keys = array_intersect($keys, $options['fields']);
|
||||
}
|
||||
|
||||
$item->searchHits = 0;
|
||||
$item->searchScore = 0;
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$score = $options['score'][$key] ?? 1;
|
||||
$value = $key === 'id' ? $item->id() : $data[$key];
|
||||
|
||||
$lowerValue = strtolower($value);
|
||||
|
||||
// check for exact matches
|
||||
if ($lowerQuery == $lowerValue) {
|
||||
$item->searchScore += 16 * $score;
|
||||
$item->searchHits += 1;
|
||||
|
||||
// check for exact beginning matches
|
||||
} elseif (Str::startsWith($lowerValue, $lowerQuery) === true) {
|
||||
$item->searchScore += 8 * $score;
|
||||
$item->searchHits += 1;
|
||||
|
||||
// check for exact query matches
|
||||
} elseif ($matches = preg_match_all('!' . preg_quote($query) . '!i', $value, $r)) {
|
||||
$item->searchScore += 2 * $score;
|
||||
$item->searchHits += $matches;
|
||||
}
|
||||
|
||||
// check for any match
|
||||
if ($matches = preg_match_all($preg, $value, $r)) {
|
||||
$item->searchHits += $matches;
|
||||
$item->searchScore += $matches * $score;
|
||||
}
|
||||
}
|
||||
|
||||
return $item->searchHits > 0 ? true : false;
|
||||
});
|
||||
|
||||
return $results->sortBy('searchScore', 'desc');
|
||||
}
|
||||
|
||||
public static function pages(string $query = null, $params = [])
|
||||
{
|
||||
return App::instance()->site()->index()->search($query, $params);
|
||||
}
|
||||
|
||||
public static function users(string $query = null, $params = [])
|
||||
{
|
||||
return App::instance()->users()->search($query, $params);
|
||||
}
|
||||
}
|
||||
67
kirby/src/Cms/Section.php
Executable file
67
kirby/src/Cms/Section.php
Executable file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Component;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public function kirby()
|
||||
{
|
||||
return $this->model->kirby();
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
675
kirby/src/Cms/Site.php
Executable file
675
kirby/src/Cms/Site.php
Executable file
@@ -0,0 +1,675 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Site class 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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
class Site extends ModelWithContent
|
||||
{
|
||||
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
|
||||
*
|
||||
* @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 SiteBlueprint
|
||||
*/
|
||||
public function blueprint(): SiteBlueprint
|
||||
{
|
||||
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
|
||||
*
|
||||
* @params 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 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
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function contentFileData(array $data, string $languageCode = null): array
|
||||
{
|
||||
return A::prepend($data, [
|
||||
'title' => $data['title'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filename for the content file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function contentFileName(): string
|
||||
{
|
||||
return 'site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error page object
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function errorPageId(): string
|
||||
{
|
||||
return $this->errorPageId ?? 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the site exists on disk
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return is_dir($this->root()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the home page object
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function homePageId(): string
|
||||
{
|
||||
return $this->homePageId ?? 'home';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an inventory of all files
|
||||
* and children in the site directory
|
||||
*
|
||||
* @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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root to the media folder for the site
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return $this->kirby()->root('media') . '/site';
|
||||
}
|
||||
|
||||
/**
|
||||
* The site's base url for any files
|
||||
*
|
||||
* @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 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 Pages
|
||||
*/
|
||||
public function pages(): Pages
|
||||
{
|
||||
return $this->children();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
return 'site';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @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 SitePermissions
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return new SitePermissions($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string query, starting from the model
|
||||
*
|
||||
* @param string|null $query
|
||||
* @param string|null $expect
|
||||
* @return mixed
|
||||
*/
|
||||
public function query(string $query = null, string $expect = null)
|
||||
{
|
||||
if ($query === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = Str::query($query, [
|
||||
'kirby' => $this->kirby(),
|
||||
'site' => $this,
|
||||
]);
|
||||
|
||||
if ($expect !== null && is_a($result, $expect) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 SiteRules
|
||||
*/
|
||||
protected function rules()
|
||||
{
|
||||
return new SiteRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all pages in the site
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $params
|
||||
* @return 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): self
|
||||
{
|
||||
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'): self
|
||||
{
|
||||
$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'): self
|
||||
{
|
||||
$this->homePageId = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current page object
|
||||
*
|
||||
* @param Page|null $page
|
||||
* @return self
|
||||
*/
|
||||
public function setPage(Page $page = null): self
|
||||
{
|
||||
$this->page = $page;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Url
|
||||
*
|
||||
* @param string $url
|
||||
* @return void
|
||||
*/
|
||||
protected function setUrl($url = null): self
|
||||
{
|
||||
$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(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* String template builder
|
||||
*
|
||||
* @param string|null $template
|
||||
* @return string
|
||||
*/
|
||||
public function toString(string $template = null): string
|
||||
{
|
||||
if ($template === null) {
|
||||
return $this->url();
|
||||
}
|
||||
|
||||
return Str::template($template, [
|
||||
'site' => $this,
|
||||
'kirby' => $this->kirby()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @params string $languageCode
|
||||
* @params 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
|
||||
*
|
||||
* @param string|Page $page
|
||||
* @param string|null $languageCode
|
||||
* @return Page
|
||||
*/
|
||||
public function visit($page, string $languageCode = null): Page
|
||||
{
|
||||
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
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function wasModifiedAfter($time): bool
|
||||
{
|
||||
return Dir::wasModifiedAfter($this->root(), $time);
|
||||
}
|
||||
}
|
||||
85
kirby/src/Cms/SiteActions.php
Executable file
85
kirby/src/Cms/SiteActions.php
Executable file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
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
|
||||
* @return mixed
|
||||
*/
|
||||
protected function commit(string $action, array $arguments, Closure $callback)
|
||||
{
|
||||
$old = $this->hardcopy();
|
||||
$kirby = $this->kirby();
|
||||
|
||||
$this->rules()->$action(...$arguments);
|
||||
$kirby->trigger('site.' . $action . ':before', ...$arguments);
|
||||
$result = $callback(...$arguments);
|
||||
$kirby->trigger('site.' . $action . ':after', $result, $old);
|
||||
$kirby->cache('pages')->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the site title
|
||||
*
|
||||
* @param string $title
|
||||
* @param string|null $languageCode
|
||||
* @return self
|
||||
*/
|
||||
public function changeTitle(string $title, string $languageCode = null): self
|
||||
{
|
||||
return $this->commit('changeTitle', [$this, $title, $languageCode], function ($site, $title, $languageCode) {
|
||||
return $site->save(['title' => $title], $languageCode);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a main page
|
||||
*
|
||||
* @param array $props
|
||||
* @return 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
|
||||
*/
|
||||
public function purge(): self
|
||||
{
|
||||
$this->children = null;
|
||||
$this->blueprint = null;
|
||||
$this->files = null;
|
||||
$this->content = null;
|
||||
$this->inventory = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
29
kirby/src/Cms/SiteBlueprint.php
Executable file
29
kirby/src/Cms/SiteBlueprint.php
Executable file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of the basic blueprint class
|
||||
* to handle the blueprint for the site.
|
||||
*/
|
||||
class SiteBlueprint extends Blueprint
|
||||
{
|
||||
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',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
8
kirby/src/Cms/SitePermissions.php
Executable file
8
kirby/src/Cms/SitePermissions.php
Executable file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
class SitePermissions extends ModelPermissions
|
||||
{
|
||||
protected $category = 'site';
|
||||
}
|
||||
30
kirby/src/Cms/SiteRules.php
Executable file
30
kirby/src/Cms/SiteRules.php
Executable file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\PermissionException;
|
||||
|
||||
/**
|
||||
* Validators for all site actions
|
||||
*/
|
||||
class SiteRules
|
||||
{
|
||||
public static function changeTitle(Site $site, string $title): bool
|
||||
{
|
||||
if ($site->permissions()->changeTitle() !== true) {
|
||||
throw new PermissionException(['key' => 'site.changeTitle.permission']);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
59
kirby/src/Cms/Structure.php
Executable file
59
kirby/src/Cms/Structure.php
Executable file
@@ -0,0 +1,59 @@
|
||||
<?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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
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 $object
|
||||
*/
|
||||
public function __set(string $id, $props)
|
||||
{
|
||||
if (is_a($props, StructureObject::class) === true) {
|
||||
$object = $props;
|
||||
} else {
|
||||
$object = new StructureObject([
|
||||
'content' => $props,
|
||||
'id' => $props['id'] ?? $id,
|
||||
'parent' => $this->parent,
|
||||
'structure' => $this
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::__set($object->id(), $object);
|
||||
}
|
||||
}
|
||||
206
kirby/src/Cms/StructureObject.php
Executable file
206
kirby/src/Cms/StructureObject.php
Executable file
@@ -0,0 +1,206 @@
|
||||
<?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 http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
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 Content
|
||||
*/
|
||||
public function content(): 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 a formatted date field from the content
|
||||
*
|
||||
* @param string $format
|
||||
* @param string $field
|
||||
* @return Field
|
||||
*/
|
||||
public function date(string $format = null, $field = 'date')
|
||||
{
|
||||
return $this->content()->get($field)->toDate($format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the required id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Model object
|
||||
*
|
||||
* @return Page|Site|File|User
|
||||
*/
|
||||
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): self
|
||||
{
|
||||
$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): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent Model. This can either be a
|
||||
* Page, Site, File or User object
|
||||
*
|
||||
* @param Page|Site|File|User|null $parent
|
||||
* @return self
|
||||
*/
|
||||
protected function setParent(Model $parent = null): self
|
||||
{
|
||||
$this->parent = $parent;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent Structure collection
|
||||
*
|
||||
* @param Structure $structure
|
||||
* @return self
|
||||
*/
|
||||
protected function setStructure(Structure $structure = null): self
|
||||
{
|
||||
$this->structure = $structure;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent Structure collection as siblings
|
||||
*
|
||||
* @return 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;
|
||||
}
|
||||
}
|
||||
446
kirby/src/Cms/System.php
Executable file
446
kirby/src/Cms/System.php
Executable file
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Throwable;
|
||||
use Kirby\Data\Json;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Http\Remote;
|
||||
use Kirby\Http\Uri;
|
||||
use Kirby\Http\Url;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class System
|
||||
{
|
||||
|
||||
/**
|
||||
* @var App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @param 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 boolean
|
||||
*/
|
||||
public function accounts(): bool
|
||||
{
|
||||
return is_writable($this->app->root('accounts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a writable content folder
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function content(): bool
|
||||
{
|
||||
return is_writable($this->app->root('content'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an existing curl extension
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function curl(): bool
|
||||
{
|
||||
return extension_loaded('curl');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the most important folders
|
||||
* if they don't exist yet
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
/* /site/accounts */
|
||||
try {
|
||||
Dir::make($this->app->root('accounts'));
|
||||
} catch (Throwable $e) {
|
||||
throw new PermissionException('The accounts directory could not be created');
|
||||
}
|
||||
|
||||
/* /content */
|
||||
try {
|
||||
Dir::make($this->app->root('content'));
|
||||
} catch (Throwable $e) {
|
||||
throw new PermissionException('The content directory could not be created');
|
||||
}
|
||||
|
||||
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 boolean
|
||||
*/
|
||||
public function isInstallable(): bool
|
||||
{
|
||||
return $this->isLocal() === true || $this->app->option('panel.install', false) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Kirby is already installed
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
return $this->app->users()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a local installation
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLocal(): bool
|
||||
{
|
||||
$server = $this->app->server();
|
||||
$host = $server->host();
|
||||
|
||||
if ($host === 'localhost') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($server->address(), ['::1', '127.0.0.1', '0.0.0.0']) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Str::endsWith($host, '.dev') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Str::endsWith($host, '.local') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Str::endsWith($host, '.test') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if all tests pass
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isOk(): bool
|
||||
{
|
||||
if ($this->isInstallable() === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(false, array_values($this->status()), true) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the app's index URL for
|
||||
* licensing purposes without scheme
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function licenseUrl(): 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 licenseUrlNormalized(string $url = null): string
|
||||
{
|
||||
if ($url === null) {
|
||||
$url = $this->licenseUrl();
|
||||
}
|
||||
|
||||
// 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|false
|
||||
*/
|
||||
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->licenseUrlNormalized() !== $this->licenseUrlNormalized($license['domain'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $license['license'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an existing mbstring extension
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function mbString(): bool
|
||||
{
|
||||
return extension_loaded('mbstring');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a writable media folder
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function media(): bool
|
||||
{
|
||||
return is_writable($this->app->root('media'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a valid PHP version
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
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 boolean
|
||||
*/
|
||||
public function register(string $license, string $email): bool
|
||||
{
|
||||
$response = Remote::get('https://licenses.getkirby.com/register', [
|
||||
'data' => [
|
||||
'license' => $license,
|
||||
'email' => $email,
|
||||
'domain' => $this->licenseUrl()
|
||||
]
|
||||
]);
|
||||
|
||||
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 Exception('The license could not be verified');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for a valid server environment
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function server(): bool
|
||||
{
|
||||
$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 boolean
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
198
kirby/src/Cms/Template.php
Executable file
198
kirby/src/Cms/Template.php
Executable file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Tpl;
|
||||
use Kirby\Toolkit\View;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Represents a Kirby template and takes care
|
||||
* of loading the correct file.
|
||||
*/
|
||||
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 boolean
|
||||
*/
|
||||
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) {
|
||||
//
|
||||
}
|
||||
|
||||
// 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 boolean
|
||||
*/
|
||||
public function hasDefaultType(): bool
|
||||
{
|
||||
$type = $this->type();
|
||||
|
||||
return $type === null || $type === $this->defaultType();
|
||||
}
|
||||
}
|
||||
171
kirby/src/Cms/Translation.php
Executable file
171
kirby/src/Cms/Translation.php
Executable file
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
|
||||
/**
|
||||
* Wrapper around Kirby's localization files,
|
||||
* which are store in `kirby/translations`.
|
||||
*/
|
||||
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 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
55
kirby/src/Cms/Translations.php
Executable file
55
kirby/src/Cms/Translations.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?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
|
||||
*/
|
||||
class Translations extends Collection
|
||||
{
|
||||
public function start(string $code)
|
||||
{
|
||||
F::move($this->parent->contentFile('', true), $this->parent->contentFile($code, true));
|
||||
}
|
||||
|
||||
public function stop(string $code)
|
||||
{
|
||||
F::move($this->parent->contentFile($code, true), $this->parent->contentFile('', true));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
45
kirby/src/Cms/Url.php
Executable file
45
kirby/src/Cms/Url.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Http\Url as BaseUrl;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Extension of the Kirby\Http\Url class
|
||||
* with a specific Url::home method that always
|
||||
* creates the correct base Url and a template asset
|
||||
* Url builder.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
787
kirby/src/Cms/User.php
Executable file
787
kirby/src/Cms/User.php
Executable file
@@ -0,0 +1,787 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Session\Session;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The User class represents
|
||||
* panel users as well as frontend users.
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
class User extends ModelWithContent
|
||||
{
|
||||
use HasFiles;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
// 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
|
||||
*
|
||||
* @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 File|null
|
||||
*/
|
||||
public function avatar()
|
||||
{
|
||||
return $this->files()->template('avatar')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the UserBlueprint object
|
||||
*
|
||||
* @return 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
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* @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 boolean
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return is_file($this->contentFile('default')) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes user password
|
||||
*
|
||||
* @param string|null $password
|
||||
* @return string|null
|
||||
*/
|
||||
public static function hashPassword($password)
|
||||
{
|
||||
if ($password !== null) {
|
||||
$info = password_get_info($password);
|
||||
|
||||
if ($info['algo'] === 0) {
|
||||
$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 User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function is(User $user): bool
|
||||
{
|
||||
return $this->id() === $user->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this user has the admin role
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->role()->id() === 'admin';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user is the virtual
|
||||
* Kirby user
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isKirby(): bool
|
||||
{
|
||||
return $this->email() === 'kirby@getkirby.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user is this user
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLoggedIn(): bool
|
||||
{
|
||||
return $this->is($this->kirby()->user());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is the last one
|
||||
* with the admin role
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
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 boolean
|
||||
*/
|
||||
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 Session|array $session Session options or session object to set the user in
|
||||
* @return bool
|
||||
*/
|
||||
public function login(string $password, $session = null): bool
|
||||
{
|
||||
try {
|
||||
$this->validatePassword($password);
|
||||
} catch (Exception $e) {
|
||||
throw new PermissionException(['key' => 'access.login']);
|
||||
}
|
||||
|
||||
$this->loginPasswordless($session);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user in without checking the password
|
||||
*
|
||||
* @param Session|array $session Session options or session object to set the user in
|
||||
* @return void
|
||||
*/
|
||||
public function loginPasswordless($session = null)
|
||||
{
|
||||
$session = $this->sessionFromOptions($session);
|
||||
|
||||
$session->regenerateToken(); // privilege change
|
||||
$session->data()->set('user.id', $this->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the user out
|
||||
*
|
||||
* @param Session|array $session Session options or session object to unset the user in
|
||||
* @return void
|
||||
*/
|
||||
public function logout($session = null)
|
||||
{
|
||||
$session = $this->sessionFromOptions($session);
|
||||
|
||||
$session->data()->remove('user.id');
|
||||
|
||||
if ($session->data()->get() === []) {
|
||||
// session is now empty, we might as well destroy it
|
||||
$session->destroy();
|
||||
} else {
|
||||
// privilege change
|
||||
$session->regenerateToken();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root to the media folder for the user
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaRoot(): string
|
||||
{
|
||||
return $this->kirby()->root('media') . '/users/' . $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the media url for the user object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function mediaUrl(): string
|
||||
{
|
||||
return $this->kirby()->url('media') . '/users/' . $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last modification date of the user
|
||||
*
|
||||
* @param string $format
|
||||
* @param string|null $handler
|
||||
* @return int|string
|
||||
*/
|
||||
public function modified(string $format = 'U', string $handler = null)
|
||||
{
|
||||
$modifiedContent = F::modified($this->contentFile());
|
||||
$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 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
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function nobody(): self
|
||||
{
|
||||
return new static([
|
||||
'email' => 'nobody@getkirby.com',
|
||||
'role' => 'nobody'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full path without leading slash
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function panelPath(): string
|
||||
{
|
||||
return 'users/' . $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url to the editing view
|
||||
* in the panel
|
||||
*
|
||||
* @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 UserPermissions
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return new UserPermissions($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a string query, starting from the model
|
||||
*
|
||||
* @param string|null $query
|
||||
* @param string|null $expect
|
||||
* @return mixed
|
||||
*/
|
||||
public function query(string $query = null, string $expect = null)
|
||||
{
|
||||
if ($query === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = Str::query($query, [
|
||||
'kirby' => $kirby = $this->kirby(),
|
||||
'site' => $kirby->site(),
|
||||
'user' => $this
|
||||
]);
|
||||
|
||||
if ($expect !== null && is_a($result, $expect) !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user role
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function role(): 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 UserRules
|
||||
*/
|
||||
protected function rules()
|
||||
{
|
||||
return new UserRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Blueprint object
|
||||
*
|
||||
* @param array|null $blueprint
|
||||
* @return self
|
||||
*/
|
||||
protected function setBlueprint(array $blueprint = null): self
|
||||
{
|
||||
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): self
|
||||
{
|
||||
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): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user language
|
||||
*
|
||||
* @param string $language
|
||||
* @return self
|
||||
*/
|
||||
protected function setLanguage(string $language = null): self
|
||||
{
|
||||
$this->language = $language !== null ? trim($language) : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user name
|
||||
*
|
||||
* @param string $name
|
||||
* @return self
|
||||
*/
|
||||
protected function setName(string $name = null): self
|
||||
{
|
||||
$this->name = $name !== null ? trim($name) : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets and hashes a new user password
|
||||
*
|
||||
* @param string $password
|
||||
* @return self
|
||||
*/
|
||||
protected function setPassword(string $password = null): self
|
||||
{
|
||||
$this->password = $password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user role
|
||||
*
|
||||
* @param string $role
|
||||
* @return self
|
||||
*/
|
||||
protected function setRole(string $role = null): self
|
||||
{
|
||||
$this->role = $role !== null ? strtolower(trim($role)) : null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts session options into a session object
|
||||
*
|
||||
* @param Session|array $session Session options or session object to unset the user in
|
||||
* @return Session
|
||||
*/
|
||||
protected function sessionFromOptions($session): 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 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
|
||||
* @return string
|
||||
*/
|
||||
public function toString(string $template = null): string
|
||||
{
|
||||
if ($template === null) {
|
||||
return $this->email();
|
||||
}
|
||||
|
||||
return Str::template($template, [
|
||||
'user' => $this,
|
||||
'site' => $this->site(),
|
||||
'kirby' => $this->kirby()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the username
|
||||
* which is the given name or the email
|
||||
* as a fallback
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function username(): ?string
|
||||
{
|
||||
return $this->name()->or($this->email())->value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the given password with the stored one
|
||||
*
|
||||
* @param string $password
|
||||
* @return boolean
|
||||
*/
|
||||
public function validatePassword(string $password = null): bool
|
||||
{
|
||||
if (empty($this->password()) === true) {
|
||||
throw new NotFoundException(['key' => 'user.password.undefined']);
|
||||
}
|
||||
|
||||
if ($password === null) {
|
||||
throw new InvalidArgumentException(['key' => 'user.password.invalid']);
|
||||
}
|
||||
|
||||
if (password_verify($password, $this->password()) !== true) {
|
||||
throw new InvalidArgumentException(['key' => 'user.password.invalid']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
295
kirby/src/Cms/UserActions.php
Executable file
295
kirby/src/Cms/UserActions.php
Executable file
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentLogicException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
trait UserActions
|
||||
{
|
||||
|
||||
/**
|
||||
* Changes the user email address
|
||||
*
|
||||
* @param string $email
|
||||
* @return self
|
||||
*/
|
||||
public function changeEmail(string $email): self
|
||||
{
|
||||
return $this->commit('changeEmail', [$this, $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): self
|
||||
{
|
||||
return $this->commit('changeLanguage', [$this, $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): self
|
||||
{
|
||||
return $this->commit('changeName', [$this, $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): self
|
||||
{
|
||||
return $this->commit('changePassword', [$this, $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): self
|
||||
{
|
||||
return $this->commit('changeRole', [$this, $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();
|
||||
|
||||
$this->rules()->$action(...$arguments);
|
||||
$this->kirby()->trigger('user.' . $action . ':before', ...$arguments);
|
||||
$result = $callback(...$arguments);
|
||||
$this->kirby()->trigger('user.' . $action . ':after', $result, $old);
|
||||
$this->kirby()->cache('pages')->flush();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new User from the given props and returns a new User object
|
||||
*
|
||||
* @param array $input
|
||||
* @return self
|
||||
*/
|
||||
public static function create(array $props = null): self
|
||||
{
|
||||
$data = $props;
|
||||
|
||||
if (isset($props['password']) === true) {
|
||||
$data['password'] = static::hashPassword($props['password']);
|
||||
}
|
||||
|
||||
$user = new static($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, $props], function ($user, $props) {
|
||||
$user->writeCredentials([
|
||||
'email' => $user->email(),
|
||||
'language' => $user->language(),
|
||||
'name' => $user->name()->value(),
|
||||
'role' => $user->role()->id(),
|
||||
]);
|
||||
|
||||
$user->writePassword($user->password());
|
||||
|
||||
// write the user data
|
||||
return $user->save();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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', [$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');
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the account information from disk
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function readCredentials(): array
|
||||
{
|
||||
if (file_exists($this->root() . '/index.php') === true) {
|
||||
$credentials = require $this->root() . '/index.php';
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
protected function writeCredentials(array $credentials): bool
|
||||
{
|
||||
$export = '<?php' . PHP_EOL . PHP_EOL . 'return ' . var_export($credentials, true) . ';';
|
||||
|
||||
return F::write($this->root() . '/index.php', $export);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the password to disk
|
||||
*
|
||||
* @param string $password
|
||||
* @return bool
|
||||
*/
|
||||
protected function writePassword(string $password = null): bool
|
||||
{
|
||||
return F::write($this->root() . '/.htpasswd', $password);
|
||||
}
|
||||
}
|
||||
31
kirby/src/Cms/UserBlueprint.php
Executable file
31
kirby/src/Cms/UserBlueprint.php
Executable file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Extension of the basic blueprint class
|
||||
* to handle all blueprints for users.
|
||||
*/
|
||||
class UserBlueprint extends Blueprint
|
||||
{
|
||||
public function __construct(array $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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
26
kirby/src/Cms/UserPermissions.php
Executable file
26
kirby/src/Cms/UserPermissions.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
class UserPermissions extends ModelPermissions
|
||||
{
|
||||
protected $category = 'users';
|
||||
|
||||
public function __construct(Model $model)
|
||||
{
|
||||
parent::__construct($model);
|
||||
|
||||
// change the scope of the permissions, when the current user is this user
|
||||
$this->category = $this->user && $this->user->is($model) ? 'user' : 'users';
|
||||
}
|
||||
|
||||
protected function canChangeRole(): bool
|
||||
{
|
||||
return $this->model->isLastAdmin() !== true;
|
||||
}
|
||||
|
||||
protected function canDelete(): bool
|
||||
{
|
||||
return $this->model->isLastAdmin() !== true;
|
||||
}
|
||||
}
|
||||
206
kirby/src/Cms/UserRules.php
Executable file
206
kirby/src/Cms/UserRules.php
Executable file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\DuplicateException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
/**
|
||||
* Validators for all user actions
|
||||
*/
|
||||
class UserRules
|
||||
{
|
||||
public static function changeEmail(User $user, string $email): bool
|
||||
{
|
||||
if ($user->permissions()->changeEmail() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'user.changeEmail.permission',
|
||||
'data' => ['name' => $user->username()]
|
||||
]);
|
||||
}
|
||||
|
||||
return static::validEmail($user, $email);
|
||||
}
|
||||
|
||||
public static function changeLanguage(User $user, string $language): bool
|
||||
{
|
||||
if ($user->permissions()->changeLanguage() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'user.changeLanguage.permission',
|
||||
'data' => ['name' => $user->username()]
|
||||
]);
|
||||
}
|
||||
|
||||
return static::validLanguage($user, $language);
|
||||
}
|
||||
|
||||
public static function changeName(User $user, string $name): bool
|
||||
{
|
||||
if ($user->permissions()->changeName() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'user.changeName.permission',
|
||||
'data' => ['name' => $user->username()]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function changePassword(User $user, string $password): bool
|
||||
{
|
||||
if ($user->permissions()->changePassword() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'user.changePassword.permission',
|
||||
'data' => ['name' => $user->username()]
|
||||
]);
|
||||
}
|
||||
|
||||
return static::validPassword($user, $password);
|
||||
}
|
||||
|
||||
public static function changeRole(User $user, string $role): bool
|
||||
{
|
||||
static::validRole($user, $role);
|
||||
|
||||
if ($role !== 'admin' && $user->isLastAdmin() === true) {
|
||||
throw new LogicException([
|
||||
'key' => 'user.changeRole.lastAdmin',
|
||||
'data' => ['name' => $user->username()]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($user->permissions()->changeRole() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'user.changeRole.permission',
|
||||
'data' => ['name' => $user->username()]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function create(User $user, array $props = []): bool
|
||||
{
|
||||
static::validId($user, $user->id());
|
||||
static::validEmail($user, $user->email(), true);
|
||||
static::validLanguage($user, $user->language());
|
||||
|
||||
if (empty($props['password']) === false) {
|
||||
static::validPassword($user, $props['password']);
|
||||
}
|
||||
|
||||
if ($user->kirby()->users()->count() > 0) {
|
||||
if ($user->permissions()->create() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'user.create.permission'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function delete(User $user): bool
|
||||
{
|
||||
if ($user->isLastAdmin() === true) {
|
||||
throw new LogicException(['key' => 'user.delete.lastAdmin']);
|
||||
}
|
||||
|
||||
if ($user->isLastUser() === true) {
|
||||
throw new LogicException([
|
||||
'key' => 'user.delete.lastUser'
|
||||
]);
|
||||
}
|
||||
|
||||
if ($user->permissions()->delete() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'user.delete.permission',
|
||||
'data' => ['name' => $user->username()]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function update(User $user, array $values = [], array $strings = []): bool
|
||||
{
|
||||
if ($user->permissions()->update() !== true) {
|
||||
throw new PermissionException([
|
||||
'key' => 'user.update.permission',
|
||||
'data' => ['name' => $user->username()]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validEmail(User $user, string $email, bool $strict = false): bool
|
||||
{
|
||||
if (V::email($email ?? null) === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'user.email.invalid',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($strict === true) {
|
||||
$duplicate = $user->kirby()->users()->find($email);
|
||||
} else {
|
||||
$duplicate = $user->kirby()->users()->not($user)->find($email);
|
||||
}
|
||||
|
||||
if ($duplicate) {
|
||||
throw new DuplicateException([
|
||||
'key' => 'user.duplicate',
|
||||
'data' => ['email' => $email]
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validId(User $user, string $id): bool
|
||||
{
|
||||
if ($duplicate = $user->kirby()->users()->find($id)) {
|
||||
throw new DuplicateException('A user with this id exists');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validLanguage(User $user, string $language): bool
|
||||
{
|
||||
if (in_array($language, $user->kirby()->translations()->keys(), true) === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'user.language.invalid',
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validPassword(User $user, string $password): bool
|
||||
{
|
||||
if (Str::length($password ?? null) < 8) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'user.password.invalid',
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function validRole(User $user, string $role): bool
|
||||
{
|
||||
if (is_a($user->kirby()->roles()->find($role), 'Kirby\Cms\Role') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'user.role.invalid',
|
||||
]);
|
||||
}
|
||||
}
|
||||
108
kirby/src/Cms/Users.php
Executable file
108
kirby/src/Cms/Users.php
Executable file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Dir;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Extension of the Collection class that
|
||||
* provides a Users::factory method to convert
|
||||
* an array into a Users collection with User
|
||||
* objects and a Users::load method to load
|
||||
* user accounts from disk.
|
||||
*/
|
||||
class Users extends Collection
|
||||
{
|
||||
public function create(array $data)
|
||||
{
|
||||
return User::create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single user or
|
||||
* an entire second collection to the
|
||||
* current collection
|
||||
*
|
||||
* @param mixed $item
|
||||
* @return Users
|
||||
*/
|
||||
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 user by id
|
||||
} elseif (is_string($object) === true && $user = App::instance()->user($object)) {
|
||||
$this->__set($user->id(), $user);
|
||||
|
||||
// add a user object
|
||||
} elseif (is_a($object, User::class) === true) {
|
||||
$this->__set($object->id(), $object);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of user props and creates a nice and clean user collection from it
|
||||
*
|
||||
* @param array $users
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function factory(array $users, array $inject = []): self
|
||||
{
|
||||
$collection = new static;
|
||||
|
||||
// read all user blueprints
|
||||
foreach ($users as $props) {
|
||||
$user = new User($props + $inject);
|
||||
$collection->set($user->id(), $user);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a user in the collection by id or email address
|
||||
*
|
||||
* @param string $key
|
||||
* @return User|null
|
||||
*/
|
||||
public function findByKey($key)
|
||||
{
|
||||
if (Str::contains($key, '@') === true) {
|
||||
return parent::findBy('email', $key);
|
||||
}
|
||||
|
||||
return parent::findByKey($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a user from disk by passing the absolute path (root)
|
||||
*
|
||||
* @param string $root
|
||||
* @param array $inject
|
||||
* @return self
|
||||
*/
|
||||
public static function load(string $root, array $inject = []): self
|
||||
{
|
||||
$users = new static;
|
||||
|
||||
foreach (Dir::read($root) as $userDirectory) {
|
||||
if (is_dir($root . '/' . $userDirectory) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$user = new User([
|
||||
'id' => $userDirectory,
|
||||
] + $inject);
|
||||
|
||||
$users->set($user->id(), $user);
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
}
|
||||
19
kirby/src/Cms/Visitor.php
Executable file
19
kirby/src/Cms/Visitor.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Facade;
|
||||
|
||||
/**
|
||||
* Shortcut to the visitor object
|
||||
*/
|
||||
class Visitor extends Facade
|
||||
{
|
||||
/**
|
||||
* @return \Kirby\Http\Visitor
|
||||
*/
|
||||
protected static function instance()
|
||||
{
|
||||
return App::instance()->visitor();
|
||||
}
|
||||
}
|
||||
124
kirby/src/Data/Data.php
Executable file
124
kirby/src/Data/Data.php
Executable file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* Universal Data writer and reader class.
|
||||
*
|
||||
* The read and write methods automatically
|
||||
* detect, which data handler to use in order
|
||||
* to correctly encode and decode passed data.
|
||||
*
|
||||
* Data Handlers for the class can be
|
||||
* extended and customized.
|
||||
*
|
||||
* @package Kirby
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright 2012 Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class Data
|
||||
{
|
||||
|
||||
/**
|
||||
* Handler Type Aliases
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $aliases = [
|
||||
'yml' => 'yaml',
|
||||
'md' => 'txt',
|
||||
'mdown' => 'txt'
|
||||
];
|
||||
|
||||
/**
|
||||
* All registered handlers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $handlers = [
|
||||
'json' => 'Kirby\Data\Json',
|
||||
'yaml' => 'Kirby\Data\Yaml',
|
||||
'txt' => 'Kirby\Data\Txt'
|
||||
];
|
||||
|
||||
/**
|
||||
* Handler getter
|
||||
*
|
||||
* @param string $type
|
||||
* @return Handler|null
|
||||
*/
|
||||
public static function handler(string $type)
|
||||
{
|
||||
// normalize the type
|
||||
$type = strtolower($type);
|
||||
$handler = static::$handlers[$type] ?? null;
|
||||
|
||||
if ($handler === null && isset(static::$aliases[$type]) === true) {
|
||||
$handler = static::$handlers[static::$aliases[$type]] ?? null;
|
||||
}
|
||||
|
||||
if ($handler === null) {
|
||||
throw new Exception('Missing Handler for type: "' . $type . '"');
|
||||
}
|
||||
|
||||
return new $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode data with the specified handler
|
||||
*
|
||||
* @param string $data
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function decode(string $data = null, string $type): array
|
||||
{
|
||||
return static::handler($type)->decode($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode data with the specified handler
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(array $data = null, string $type): string
|
||||
{
|
||||
return static::handler($type)->encode($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads data from a file
|
||||
* The data handler is automatically chosen by
|
||||
* the extension if not specified.
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function read(string $file, string $type = null): array
|
||||
{
|
||||
return static::handler($type ?? F::extension($file))->read($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes data to a file.
|
||||
* The data handler is automatically chosen by
|
||||
* the extension if not specified.
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $data
|
||||
* @param string $type
|
||||
* @return boolean
|
||||
*/
|
||||
public static function write(string $file = null, array $data = [], string $type = null): bool
|
||||
{
|
||||
return static::handler($type ?? F::extension($file))->write($file, $data);
|
||||
}
|
||||
}
|
||||
67
kirby/src/Data/Handler.php
Executable file
67
kirby/src/Data/Handler.php
Executable file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Toolkit\F;
|
||||
|
||||
/**
|
||||
* Base handler abstract,
|
||||
* which needs to be extended to
|
||||
* create valid data handlers
|
||||
*
|
||||
* @package Kirby Data
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
abstract class Handler
|
||||
{
|
||||
|
||||
/**
|
||||
* Parses an encoded string and returns a multi-dimensional array
|
||||
*
|
||||
* Needs to throw an Exception if the file can't be parsed.
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
abstract public static function decode($string): array;
|
||||
|
||||
/**
|
||||
* Converts an array to an encoded string
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
abstract public static function encode(array $data): string;
|
||||
|
||||
/**
|
||||
* Reads data from a file
|
||||
*
|
||||
* @param string $file
|
||||
* @return array
|
||||
*/
|
||||
public static function read(string $file): array
|
||||
{
|
||||
if (file_exists($file) !== true) {
|
||||
throw new Exception('The file "' . $file . '" does not exist');
|
||||
}
|
||||
|
||||
return static::decode(F::read($file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes data to a file.
|
||||
* The data handler is automatically chosen by
|
||||
* the extension if not specified.
|
||||
*
|
||||
* @param array $data
|
||||
* @return boolean
|
||||
*/
|
||||
public static function write(string $file = null, array $data = []): bool
|
||||
{
|
||||
return F::write($file, static::encode($data));
|
||||
}
|
||||
}
|
||||
45
kirby/src/Data/Json.php
Executable file
45
kirby/src/Data/Json.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Simple Wrapper around json_encode and json_decode
|
||||
*
|
||||
* @package Kirby Data
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class Json extends Handler
|
||||
{
|
||||
/**
|
||||
* Converts an array to an encoded JSON string
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(array $data): string
|
||||
{
|
||||
return json_encode($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an encoded JSON string and returns a multi-dimensional array
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($json): array
|
||||
{
|
||||
$result = json_decode($json, true);
|
||||
|
||||
if (is_array($result) === true) {
|
||||
return $result;
|
||||
} else {
|
||||
throw new Exception('JSON string is invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
112
kirby/src/Data/Txt.php
Executable file
112
kirby/src/Data/Txt.php
Executable file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Kirby Txt Data Handler
|
||||
*
|
||||
* @package Kirby Data
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class Txt extends Handler
|
||||
{
|
||||
|
||||
/**
|
||||
* Converts an array to an encoded Kirby txt string
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(array $data): string
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (empty($key) === true || $value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = Str::ucfirst(Str::slug($key));
|
||||
$value = static::encodeValue($value);
|
||||
$result[$key] = static::encodeResult($key, $value);
|
||||
}
|
||||
|
||||
return implode("\n\n----\n\n", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for converting value
|
||||
*
|
||||
* @param array|string $value
|
||||
* @return string
|
||||
*/
|
||||
protected static function encodeValue($value): string
|
||||
{
|
||||
// avoid problems with arrays
|
||||
if (is_array($value) === true) {
|
||||
$value = Yaml::encode($value);
|
||||
}
|
||||
|
||||
// escape accidental dividers within a field
|
||||
$value = preg_replace('!(\n|^)----(.*?\R*)!', '$1\\----$2', $value);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for converting key and value to result string
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
protected static function encodeResult(string $key, string $value): string
|
||||
{
|
||||
$result = $key . ': ';
|
||||
|
||||
// multi-line content
|
||||
if (preg_match('!\R!', $value) === 1) {
|
||||
$result .= "\n\n";
|
||||
}
|
||||
|
||||
$result .= trim($value);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Kirby txt string and returns a multi-dimensional array
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($string): array
|
||||
{
|
||||
// remove BOM
|
||||
$string = str_replace("\xEF\xBB\xBF", '', $string);
|
||||
// explode all fields by the line separator
|
||||
$fields = preg_split('!\n----\s*\n*!', $string);
|
||||
// start the data array
|
||||
$data = [];
|
||||
|
||||
// loop through all fields and add them to the content
|
||||
foreach ($fields as $field) {
|
||||
$pos = strpos($field, ':');
|
||||
$key = str_replace(['-', ' '], '_', strtolower(trim(substr($field, 0, $pos))));
|
||||
|
||||
// Don't add fields with empty keys
|
||||
if (empty($key) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$key] = trim(substr($field, $pos + 1));
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
56
kirby/src/Data/Yaml.php
Executable file
56
kirby/src/Data/Yaml.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Data;
|
||||
|
||||
use Exception;
|
||||
use Spyc;
|
||||
|
||||
/**
|
||||
* Simple Wrapper around Symfony's Yaml Component
|
||||
*
|
||||
* @package Kirby Data
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class Yaml extends Handler
|
||||
{
|
||||
|
||||
/**
|
||||
* Converts an array to an encoded YAML string
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(array $data): string
|
||||
{
|
||||
// $data, $indent, $wordwrap, $no_opening_dashes
|
||||
return Spyc::YAMLDump($data, false, false, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an encoded YAML string and returns a multi-dimensional array
|
||||
*
|
||||
* @param string $string
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($yaml): array
|
||||
{
|
||||
if ($yaml === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_array($yaml) === true) {
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
$result = Spyc::YAMLLoadString($yaml);
|
||||
|
||||
if (is_array($result)) {
|
||||
return $result;
|
||||
} else {
|
||||
throw new Exception('YAML string is invalid');
|
||||
}
|
||||
}
|
||||
}
|
||||
621
kirby/src/Database/Database.php
Executable file
621
kirby/src/Database/Database.php
Executable file
@@ -0,0 +1,621 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Database;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* A simple database class
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
|
||||
/**
|
||||
* The number of affected rows for the last query
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $affected;
|
||||
|
||||
/**
|
||||
* Whitelist for column names
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $columnWhitelist = [];
|
||||
|
||||
/**
|
||||
* The established connection
|
||||
*
|
||||
* @var PDO|null
|
||||
*/
|
||||
protected $connection;
|
||||
|
||||
/**
|
||||
* A global array of started connections
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $connections = [];
|
||||
|
||||
/**
|
||||
* Database name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $dsn;
|
||||
|
||||
/**
|
||||
* Set to true to throw exceptions on failed queries
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $fail = false;
|
||||
|
||||
/**
|
||||
* The connection id
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The last error
|
||||
*
|
||||
* @var Exception|null
|
||||
*/
|
||||
protected $lastError;
|
||||
|
||||
/**
|
||||
* The last insert id
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $lastId;
|
||||
|
||||
/**
|
||||
* The last query
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $lastQuery;
|
||||
|
||||
/**
|
||||
* The last result set
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
protected $lastResult;
|
||||
|
||||
/**
|
||||
* Optional prefix for table names
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $prefix;
|
||||
|
||||
/**
|
||||
* The PDO query statement
|
||||
*
|
||||
* @var PDOStatement|null
|
||||
*/
|
||||
protected $statement;
|
||||
|
||||
/**
|
||||
* Whitelists for table names
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $tableWhitelist;
|
||||
|
||||
/**
|
||||
* An array with all queries which are being made
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $trace = [];
|
||||
|
||||
/**
|
||||
* The database type (mysql, sqlite)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $types = [];
|
||||
|
||||
/**
|
||||
* Creates a new Database instance
|
||||
*
|
||||
* @param array $params
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->connect($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one of the started instance
|
||||
*
|
||||
* @param string $id
|
||||
* @return Database
|
||||
*/
|
||||
public static function instance(string $id = null): self
|
||||
{
|
||||
return $id === null ? A::last(static::$connections) : static::$connections[$id] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all started instances
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function instances(): array
|
||||
{
|
||||
return static::$connections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to a database
|
||||
*
|
||||
* @param array|null $params This can either be a config key or an array of parameters for the connection
|
||||
* @return Database
|
||||
*/
|
||||
public function connect(array $params = null)
|
||||
{
|
||||
$defaults = [
|
||||
'database' => null,
|
||||
'type' => 'mysql',
|
||||
'prefix' => null,
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'id' => uniqid()
|
||||
];
|
||||
|
||||
$options = array_merge($defaults, $params);
|
||||
|
||||
// store the database information
|
||||
$this->database = $options['database'];
|
||||
$this->type = $options['type'];
|
||||
$this->prefix = $options['prefix'];
|
||||
$this->id = $options['id'];
|
||||
|
||||
if (isset(static::$types[$this->type]) === false) {
|
||||
throw new InvalidArgumentException('Invalid database type: ' . $this->type);
|
||||
}
|
||||
|
||||
// fetch the dsn and store it
|
||||
$this->dsn = static::$types[$this->type]['dsn']($options);
|
||||
|
||||
// try to connect
|
||||
$this->connection = new PDO($this->dsn, $options['user'], $options['password']);
|
||||
$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
|
||||
|
||||
// store the connection
|
||||
static::$connections[$this->id] = $this;
|
||||
|
||||
// return the connection
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently active connection
|
||||
*
|
||||
* @return Database|null
|
||||
*/
|
||||
public function connection()
|
||||
{
|
||||
return $this->connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the exception mode for the next query
|
||||
*
|
||||
* @param boolean $fail
|
||||
* @return Database
|
||||
*/
|
||||
public function fail(bool $fail = true)
|
||||
{
|
||||
$this->fail = $fail;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the used database type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the used table name prefix
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function prefix(): ?string
|
||||
{
|
||||
return $this->prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a value to be used for a safe query
|
||||
* NOTE: Prepared statements using bound parameters are more secure and solid
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public function escape(string $value): string
|
||||
{
|
||||
return substr($this->connection()->quote($value), 1, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a value to the db trace and also returns the entire trace if nothing is specified
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function trace($data = null): array
|
||||
{
|
||||
// return the full trace
|
||||
if ($data === null) {
|
||||
return $this->trace;
|
||||
}
|
||||
|
||||
// add a new entry to the trace
|
||||
$this->trace[] = $data;
|
||||
|
||||
return $this->trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of affected rows for the last query
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function affected(): ?int
|
||||
{
|
||||
return $this->affected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last id if available
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function lastId(): ?int
|
||||
{
|
||||
return $this->lastId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last query
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function lastQuery(): ?string
|
||||
{
|
||||
return $this->lastQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last set of results
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function lastResult()
|
||||
{
|
||||
return $this->lastResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last db error
|
||||
*
|
||||
* @return Throwable
|
||||
*/
|
||||
public function lastError()
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private method to execute database queries.
|
||||
* This is used by the query() and execute() methods
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $bindings
|
||||
* @return boolean
|
||||
*/
|
||||
protected function hit(string $query, array $bindings = []): bool
|
||||
{
|
||||
|
||||
// try to prepare and execute the sql
|
||||
try {
|
||||
$this->statement = $this->connection->prepare($query);
|
||||
$this->statement->execute($bindings);
|
||||
|
||||
$this->affected = $this->statement->rowCount();
|
||||
$this->lastId = $this->connection->lastInsertId();
|
||||
$this->lastError = null;
|
||||
|
||||
// store the final sql to add it to the trace later
|
||||
$this->lastQuery = $this->statement->queryString;
|
||||
} catch (Throwable $e) {
|
||||
|
||||
// store the error
|
||||
$this->affected = 0;
|
||||
$this->lastError = $e;
|
||||
$this->lastId = null;
|
||||
$this->lastQuery = $query;
|
||||
|
||||
// only throw the extension if failing is allowed
|
||||
if ($this->fail === true) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// add a new entry to the singleton trace array
|
||||
$this->trace([
|
||||
'query' => $this->lastQuery,
|
||||
'bindings' => $bindings,
|
||||
'error' => $this->lastError
|
||||
]);
|
||||
|
||||
// reset some stuff
|
||||
$this->fail = false;
|
||||
|
||||
// return true or false on success or failure
|
||||
return $this->lastError === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exectues a sql query, which is expected to return a set of results
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $bindings
|
||||
* @param array $params
|
||||
* @return mixed
|
||||
*/
|
||||
public function query(string $query, array $bindings = [], array $params = [])
|
||||
{
|
||||
$defaults = [
|
||||
'flag' => null,
|
||||
'method' => 'fetchAll',
|
||||
'fetch' => 'Kirby\Toolkit\Obj',
|
||||
'iterator' => 'Kirby\Toolkit\Collection',
|
||||
];
|
||||
|
||||
$options = array_merge($defaults, $params);
|
||||
|
||||
if ($this->hit($query, $bindings) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// define the default flag for the fetch method
|
||||
$flags = $options['fetch'] === 'array' ? PDO::FETCH_ASSOC : PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE;
|
||||
|
||||
// add optional flags
|
||||
if (empty($options['flag']) === false) {
|
||||
$flags |= $options['flag'];
|
||||
}
|
||||
|
||||
// set the fetch mode
|
||||
if ($options['fetch'] === 'array') {
|
||||
$this->statement->setFetchMode($flags);
|
||||
} else {
|
||||
$this->statement->setFetchMode($flags, $options['fetch']);
|
||||
}
|
||||
|
||||
// fetch that stuff
|
||||
$results = $this->statement->{$options['method']}();
|
||||
|
||||
if ($options['iterator'] === 'array') {
|
||||
return $this->lastResult = $results;
|
||||
}
|
||||
|
||||
return $this->lastResult = new $options['iterator']($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a sql query, which is expected to not return a set of results
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $bindings
|
||||
* @return boolean
|
||||
*/
|
||||
public function execute(string $query, array $bindings = []): bool
|
||||
{
|
||||
return $this->lastResult = $this->hit($query, $bindings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the correct Sql generator instance
|
||||
* for the type of database
|
||||
*
|
||||
* @return Sql
|
||||
*/
|
||||
public function sql()
|
||||
{
|
||||
$className = static::$types[$this->type]['sql'] ?? 'Sql';
|
||||
return new $className($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current table, which should be queried
|
||||
*
|
||||
* @param string $table
|
||||
* @return Query Returns a Query object, which can be used to build a full query for that table
|
||||
*/
|
||||
public function table(string $table)
|
||||
{
|
||||
return new Query($this, $this->prefix() . $table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a table exists in the current database
|
||||
*
|
||||
* @param string $table
|
||||
* @return boolean
|
||||
*/
|
||||
public function validateTable(string $table): bool
|
||||
{
|
||||
if ($this->tableWhitelist === null) {
|
||||
// Get the table whitelist from the database
|
||||
$sql = $this->sql()->tables($this->database);
|
||||
$results = $this->query($sql['query'], $sql['bindings']);
|
||||
|
||||
if ($results) {
|
||||
$this->tableWhitelist = $results->pluck('name');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return in_array($table, $this->tableWhitelist) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a column exists in a specified table
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $column
|
||||
* @return boolean
|
||||
*/
|
||||
public function validateColumn(string $table, string $column): bool
|
||||
{
|
||||
if (isset($this->columnWhitelist[$table]) === false) {
|
||||
if ($this->validateTable($table) === false) {
|
||||
$this->columnWhitelist[$table] = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the column whitelist from the database
|
||||
$sql = $this->sql()->columns($table);
|
||||
$results = $this->query($sql['query'], $sql['bindings']);
|
||||
|
||||
if ($results) {
|
||||
$this->columnWhitelist[$table] = $results->pluck('name');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return in_array($column, $this->columnWhitelist[$table]) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new table
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $columns
|
||||
* @return boolean
|
||||
*/
|
||||
public function createTable($table, $columns = []): bool
|
||||
{
|
||||
$sql = $this->sql()->createTable($table, $columns);
|
||||
$queries = Str::split($sql['query'], ';');
|
||||
|
||||
foreach ($queries as $query) {
|
||||
$query = trim($query);
|
||||
|
||||
if ($this->execute($query, $sql['bindings']) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a table
|
||||
*
|
||||
* @param string $table
|
||||
* @return boolean
|
||||
*/
|
||||
public function dropTable($table): bool
|
||||
{
|
||||
$sql = $this->sql()->dropTable($table);
|
||||
return $this->execute($sql['query'], $sql['bindings']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic way to start queries for tables by
|
||||
* using a method named like the table.
|
||||
* I.e. $db->users()->all()
|
||||
*/
|
||||
public function __call($method, $arguments = null)
|
||||
{
|
||||
return $this->table($method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL database connector
|
||||
*/
|
||||
Database::$types['mysql'] = [
|
||||
'sql' => 'Kirby\Database\Sql\Mysql',
|
||||
'dsn' => function (array $params) {
|
||||
if (isset($params['host']) === false && isset($params['socket']) === false) {
|
||||
throw new InvalidArgumentException('The mysql connection requires either a "host" or a "socket" parameter');
|
||||
}
|
||||
|
||||
if (isset($params['database']) === false) {
|
||||
throw new InvalidArgumentException('The mysql connection requires a "database" parameter');
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
|
||||
if (empty($params['host']) === false) {
|
||||
$parts[] = 'host=' . $params['host'];
|
||||
}
|
||||
|
||||
if (empty($params['port']) === false) {
|
||||
$parts[] = 'port=' . $params['port'];
|
||||
}
|
||||
|
||||
if (empty($params['socket']) === false) {
|
||||
$parts[] = 'unix_socket=' . $params['socket'];
|
||||
}
|
||||
|
||||
if (empty($params['database']) === false) {
|
||||
$parts[] = 'dbname=' . $params['database'];
|
||||
}
|
||||
|
||||
$parts[] = 'charset=' . ($params['charset'] ?? 'utf8');
|
||||
|
||||
return 'mysql:' . implode(';', $parts);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* SQLite database connector
|
||||
*/
|
||||
Database::$types['sqlite'] = [
|
||||
'sql' => 'Kirby\Database\Sql\Sqlite',
|
||||
'dsn' => function (array $params) {
|
||||
if (isset($params['database']) === false) {
|
||||
throw new InvalidArgumentException('The sqlite connection requires a "database" parameter');
|
||||
}
|
||||
|
||||
return 'sqlite:' . $params['database'];
|
||||
}
|
||||
];
|
||||
262
kirby/src/Database/Db.php
Executable file
262
kirby/src/Database/Db.php
Executable file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Database;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Kirby\Toolkit\Config;
|
||||
|
||||
/**
|
||||
* Database shortcuts
|
||||
*/
|
||||
class Db
|
||||
{
|
||||
const ERROR_UNKNOWN_METHOD = 0;
|
||||
|
||||
/**
|
||||
* Query shortcuts
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $queries = [];
|
||||
|
||||
/**
|
||||
* The singleton Database object
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
public static $connection = null;
|
||||
|
||||
/**
|
||||
* (Re)connect the database
|
||||
*
|
||||
* @param array $params Pass [] to use the default params from the config
|
||||
* @return Database
|
||||
*/
|
||||
public static function connect(array $params = null)
|
||||
{
|
||||
if ($params === null && static::$connection !== null) {
|
||||
return static::$connection;
|
||||
}
|
||||
|
||||
// try to connect with the default
|
||||
// connection settings if no params are set
|
||||
$params = $params ?? [
|
||||
'type' => Config::get('db.type', 'mysql'),
|
||||
'host' => Config::get('db.host', 'localhost'),
|
||||
'user' => Config::get('db.user', 'root'),
|
||||
'password' => Config::get('db.password', ''),
|
||||
'database' => Config::get('db.database', ''),
|
||||
'prefix' => Config::get('db.prefix', ''),
|
||||
];
|
||||
|
||||
return static::$connection = new Database($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current database connection
|
||||
*
|
||||
* @return Database
|
||||
*/
|
||||
public static function connection()
|
||||
{
|
||||
return static::$connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current table, which should be queried
|
||||
*
|
||||
* @param string $table
|
||||
* @return Query Returns a Query object, which can be used to build a full query for that table
|
||||
*/
|
||||
public static function table($table)
|
||||
{
|
||||
$db = static::connect();
|
||||
return $db->table($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a raw sql query which expects a set of results
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $bindings
|
||||
* @param array $params
|
||||
* @return mixed
|
||||
*/
|
||||
public static function query(string $query, array $bindings = [], array $params = [])
|
||||
{
|
||||
$db = static::connect();
|
||||
return $db->query($query, $bindings, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a raw sql query which expects no set of results (i.e. update, insert, delete)
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $bindings
|
||||
* @return mixed
|
||||
*/
|
||||
public static function execute(string $query, array $bindings = [])
|
||||
{
|
||||
$db = static::connect();
|
||||
return $db->execute($query, $bindings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic calls for other static db methods,
|
||||
* which are redircted to the database class if available
|
||||
*
|
||||
* @param string $method
|
||||
* @param mixed $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public static function __callStatic($method, $arguments)
|
||||
{
|
||||
if (isset(static::$queries[$method])) {
|
||||
return static::$queries[$method](...$arguments);
|
||||
}
|
||||
|
||||
if (is_callable([static::$connection, $method]) === true) {
|
||||
return call_user_func_array([static::$connection, $method], $arguments);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Invalid static Db method: ' . $method, static::ERROR_UNKNOWN_METHOD);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for select clauses
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param mixed $columns Either a string with columns or an array of column names
|
||||
* @param mixed $where The where clause. Can be a string or an array
|
||||
* @param string $order
|
||||
* @param int $offset
|
||||
* @param int $limit
|
||||
* @return mixed
|
||||
*/
|
||||
Db::$queries['select'] = function (string $table, $columns = '*', $where = null, string $order = null, int $offset = 0, int $limit = null) {
|
||||
return Db::table($table)->select($columns)->where($where)->order($order)->offset($offset)->limit($limit)->all();
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for selecting a single row in a table
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param mixed $columns Either a string with columns or an array of column names
|
||||
* @param mixed $where The where clause. Can be a string or an array
|
||||
* @param string $order
|
||||
* @param int $offset
|
||||
* @param int $limit
|
||||
* @return mixed
|
||||
*/
|
||||
Db::$queries['first'] = Db::$queries['row'] = Db::$queries['one'] = function (string $table, $columns = '*', $where = null, string $order = null) {
|
||||
return Db::table($table)->select($columns)->where($where)->order($order)->first();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns only values from a single column
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $column The name of the column to select from
|
||||
* @param mixed $where The where clause. Can be a string or an array
|
||||
* @param string $order
|
||||
* @param int $offset
|
||||
* @param int $limit
|
||||
* @return mixed
|
||||
*/
|
||||
Db::$queries['column'] = function (string $table, string $column, $where = null, string $order = null, int $offset = 0, int $limit = null) {
|
||||
return Db::table($table)->where($where)->order($order)->offset($offset)->limit($limit)->column($column);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for inserting a new row into a table
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param array $values An array of values, which should be inserted
|
||||
* @return boolean
|
||||
*/
|
||||
Db::$queries['insert'] = function (string $table, array $values) {
|
||||
return Db::table($table)->insert($values);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for updating a row in a table
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param array $values An array of values, which should be inserted
|
||||
* @param mixed $where An optional where clause
|
||||
* @return boolean
|
||||
*/
|
||||
Db::$queries['update'] = function (string $table, array $values, $where = null) {
|
||||
return Db::table($table)->where($where)->update($values);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for deleting rows in a table
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param mixed $where An optional where clause
|
||||
* @return boolean
|
||||
*/
|
||||
Db::$queries['delete'] = function (string $table, $where = null) {
|
||||
return Db::table($table)->where($where)->delete();
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for counting rows in a table
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param mixed $where An optional where clause
|
||||
* @return int
|
||||
*/
|
||||
Db::$queries['count'] = function (string $table, $where = null) {
|
||||
return Db::table($table)->where($where)->count();
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for calculating the minimum value in a column
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $column The name of the column of which the minimum should be calculated
|
||||
* @param mixed $where An optional where clause
|
||||
* @return mixed
|
||||
*/
|
||||
Db::$queries['min'] = function (string $table, string $column, $where = null) {
|
||||
return Db::table($table)->where($where)->min($column);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for calculating the maximum value in a column
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $column The name of the column of which the maximum should be calculated
|
||||
* @param mixed $where An optional where clause
|
||||
* @return mixed
|
||||
*/
|
||||
Db::$queries['max'] = function (string $table, string $column, $where = null) {
|
||||
return Db::table($table)->where($where)->max($column);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for calculating the average value in a column
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $column The name of the column of which the average should be calculated
|
||||
* @param mixed $where An optional where clause
|
||||
* @return mixed
|
||||
*/
|
||||
Db::$queries['avg'] = function (string $table, string $column, $where = null) {
|
||||
return Db::table($table)->where($where)->avg($column);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shortcut for calculating the sum of all values in a column
|
||||
*
|
||||
* @param string $table The name of the table, which should be queried
|
||||
* @param string $column The name of the column of which the sum should be calculated
|
||||
* @param mixed $where An optional where clause
|
||||
* @return mixed
|
||||
*/
|
||||
Db::$queries['sum'] = function (string $table, string $column, $where = null) {
|
||||
return Db::table($table)->where($where)->sum($column);
|
||||
};
|
||||
1055
kirby/src/Database/Query.php
Executable file
1055
kirby/src/Database/Query.php
Executable file
File diff suppressed because it is too large
Load Diff
929
kirby/src/Database/Sql.php
Executable file
929
kirby/src/Database/Sql.php
Executable file
@@ -0,0 +1,929 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Database;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* SQL Query builder
|
||||
*/
|
||||
class Sql
|
||||
{
|
||||
|
||||
/**
|
||||
* List of literals which should not be escaped in queries
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $literals = ['NOW()', null];
|
||||
|
||||
/**
|
||||
* The parent database connection
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
public $database;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param Database $database
|
||||
*/
|
||||
public function __construct($database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a randomly generated binding name
|
||||
*
|
||||
* @param string $label String that contains lowercase letters and numbers to use as a readable identifier
|
||||
* @param string $prefix
|
||||
* @return string
|
||||
*/
|
||||
public function bindingName(string $label): string
|
||||
{
|
||||
// make sure that the binding name is valid to prevent injections
|
||||
if (!preg_match('/^[a-z0-9_]+$/', $label)) {
|
||||
$label = 'invalid';
|
||||
}
|
||||
|
||||
return ':' . $label . '_' . Str::random(16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of columns for a specified table
|
||||
* MySQL version
|
||||
*
|
||||
* @param string $table The table name
|
||||
* @return array
|
||||
*/
|
||||
public function columns(string $table): array
|
||||
{
|
||||
$databaseBinding = $this->bindingName('database');
|
||||
$tableBinding = $this->bindingName('table');
|
||||
|
||||
$query = 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS ';
|
||||
$query .= 'WHERE TABLE_SCHEMA = ' . $databaseBinding . ' AND TABLE_NAME = ' . $tableBinding;
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'bindings' => [
|
||||
$databaseBinding => $this->database->database,
|
||||
$tableBinding => $table,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionl default value definition for the column
|
||||
*
|
||||
* @param array $column
|
||||
* @return array
|
||||
*/
|
||||
public function columnDefault(array $column): array
|
||||
{
|
||||
if (isset($column['default']) === false) {
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
$binding = $this->bindingName($column['name'] . '_default');
|
||||
|
||||
return [
|
||||
'query' => 'DEFAULT ' . $binding,
|
||||
'bindings' => [
|
||||
$binding = $column['default']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid column name
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $column
|
||||
* @param boolean $enforceQualified
|
||||
* @return string|null
|
||||
*/
|
||||
public function columnName(string $table, string $column, bool $enforceQualified = false): ?string
|
||||
{
|
||||
list($table, $column) = $this->splitIdentifier($table, $column);
|
||||
|
||||
if ($this->validateColumn($table, $column) === true) {
|
||||
return $this->combineIdentifier($table, $column, $enforceQualified !== true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstracted column types to simplify table
|
||||
* creation for multiple database drivers
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function columnTypes(): array
|
||||
{
|
||||
return [
|
||||
'id' => '{{ name }} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT',
|
||||
'varchar' => '{{ name }} varchar(255) {{ null }} {{ default }}',
|
||||
'text' => '{{ name }} TEXT',
|
||||
'int' => '{{ name }} INT(11) UNSIGNED {{ null }} {{ default }}',
|
||||
'timestamp' => '{{ name }} TIMESTAMP {{ null }} {{ default }}'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional key definition for the column.
|
||||
*
|
||||
* @param array $column
|
||||
* @return array
|
||||
*/
|
||||
public function columnKey(array $column): array
|
||||
{
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines an identifier (table and column)
|
||||
* Default version for MySQL
|
||||
*
|
||||
* @param $table string
|
||||
* @param $column string
|
||||
* @param $values boolean Whether the identifier is going to be used for a values clause
|
||||
* Only relevant for SQLite
|
||||
* @return string
|
||||
*/
|
||||
public function combineIdentifier(string $table, string $column, bool $values = false): string
|
||||
{
|
||||
return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the create syntax for a single column
|
||||
*
|
||||
* @param string $table
|
||||
* @param array $column
|
||||
* @return array
|
||||
*/
|
||||
public function createColumn(string $table, array $column): array
|
||||
{
|
||||
// column type
|
||||
if (isset($column['type']) === false) {
|
||||
throw new InvalidArgumentException('No column type given for column ' . $column);
|
||||
}
|
||||
|
||||
// column name
|
||||
if (isset($column['name']) === false) {
|
||||
throw new InvalidArgumentException('No column name given');
|
||||
}
|
||||
|
||||
if ($column['type'] === 'id') {
|
||||
$column['key'] = 'PRIMARY';
|
||||
}
|
||||
|
||||
if (!$template = ($this->columnTypes()[$column['type']] ?? null)) {
|
||||
throw new InvalidArgumentException('Unsupported column type: ' . $column['type']);
|
||||
}
|
||||
|
||||
// null
|
||||
if (A::get($column, 'null') === false) {
|
||||
$null = 'NOT NULL';
|
||||
} else {
|
||||
$null = 'NULL';
|
||||
}
|
||||
|
||||
// indexes/keys
|
||||
$key = false;
|
||||
|
||||
if (isset($column['key']) === true) {
|
||||
$column['key'] = strtoupper($column['key']);
|
||||
|
||||
// backwards compatibility
|
||||
if ($column['key'] === 'PRIMARY') {
|
||||
$column['key'] = 'PRIMARY KEY';
|
||||
}
|
||||
|
||||
if (in_array($column['key'], ['PRIMARY KEY', 'INDEX']) === true) {
|
||||
$key = $column['key'];
|
||||
}
|
||||
}
|
||||
|
||||
// default value
|
||||
$columnDefault = $this->columnDefault($column);
|
||||
$columnKey = $this->columnKey($column);
|
||||
|
||||
$query = trim(Str::template($template, [
|
||||
'name' => $this->quoteIdentifier($column['name']),
|
||||
'null' => $null,
|
||||
'key' => $columnKey['query'],
|
||||
'default' => $columnDefault['query'],
|
||||
]));
|
||||
|
||||
$bindings = array_merge($columnKey['bindings'], $columnDefault['bindings']);
|
||||
|
||||
return [
|
||||
'query' => $query,
|
||||
'bindings' => $bindings,
|
||||
'key' => $key
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a table with a simple scheme array for columns
|
||||
* Default version for MySQL
|
||||
*
|
||||
* @param string $table The table name
|
||||
* @param array $columns
|
||||
* @return array
|
||||
*/
|
||||
public function createTable(string $table, array $columns = []): array
|
||||
{
|
||||
$output = [];
|
||||
$keys = [];
|
||||
$bindings = [];
|
||||
|
||||
foreach ($columns as $name => $column) {
|
||||
$sql = $this->createColumn($table, $column);
|
||||
|
||||
$output[] = $sql['query'];
|
||||
|
||||
if ($sql['key']) {
|
||||
$keys[$column['name']] = $sql['key'];
|
||||
}
|
||||
|
||||
$bindings = array_merge($bindings, $sql['bindings']);
|
||||
}
|
||||
|
||||
// combine columns
|
||||
$inner = implode(',' . PHP_EOL, $output);
|
||||
|
||||
// add keys
|
||||
foreach ($keys as $name => $key) {
|
||||
$inner .= ',' . PHP_EOL . $key . ' (' . $this->quoteIdentifier($name) . ')';
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner . PHP_EOL . ')',
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a delete clause
|
||||
*
|
||||
* @param array $params List of parameters for the delete clause. See defaults for more info.
|
||||
* @return array
|
||||
*/
|
||||
public function delete(array $params = []): array
|
||||
{
|
||||
$defaults = [
|
||||
'table' => '',
|
||||
'where' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
|
||||
$options = array_merge($defaults, $params);
|
||||
$bindings = $options['bindings'];
|
||||
$query = ['DELETE'];
|
||||
|
||||
// from
|
||||
$this->extend($query, $bindings, $this->from($options['table']));
|
||||
|
||||
// where
|
||||
$this->extend($query, $bindings, $this->where($options['where']));
|
||||
|
||||
return [
|
||||
'query' => $this->query($query),
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the sql for dropping a single table
|
||||
*
|
||||
* @param string $table
|
||||
* @return array
|
||||
*/
|
||||
public function dropTable(string $table): array
|
||||
{
|
||||
return [
|
||||
'query' => 'DROP TABLE ' . $this->tableName($table),
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends a given query and bindings
|
||||
* by reference
|
||||
*
|
||||
* @param array $query
|
||||
* @param array $bindings
|
||||
* @param array $input
|
||||
* @return void
|
||||
*/
|
||||
public function extend(&$query, array &$bindings = [], $input)
|
||||
{
|
||||
if (empty($input['query']) === false) {
|
||||
$query[] = $input['query'];
|
||||
$bindings = array_merge($bindings, $input['bindings']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the from syntax
|
||||
*
|
||||
* @param string $table
|
||||
* @return array
|
||||
*/
|
||||
public function from(string $table): array
|
||||
{
|
||||
return [
|
||||
'query' => 'FROM ' . $this->tableName($table),
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the group by syntax
|
||||
*
|
||||
* @param string $group
|
||||
* @return array
|
||||
*/
|
||||
public function group(string $group = null): array
|
||||
{
|
||||
if (empty($group) === true) {
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => 'GROUP BY ' . $group,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the having syntax
|
||||
*
|
||||
* @param string $having
|
||||
* @return array
|
||||
*/
|
||||
public function having(string $having = null): array
|
||||
{
|
||||
if (empty($having) === true) {
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => 'HAVING ' . $having,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an insert query
|
||||
*
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function insert(array $params = []): array
|
||||
{
|
||||
$table = $params['table'] ?? null;
|
||||
$values = $params['values'] ?? null;
|
||||
$bindings = $params['bindings'];
|
||||
$query = ['INSERT INTO ' . $this->tableName($table)];
|
||||
|
||||
// add the values
|
||||
$this->extend($query, $bindings, $this->values($table, $values, ', ', false));
|
||||
|
||||
return [
|
||||
'query' => $this->query($query),
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a join query
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $type
|
||||
* @param string $on
|
||||
* @return array
|
||||
*/
|
||||
public function join(string $type, string $table, string $on): array
|
||||
{
|
||||
$types = [
|
||||
'JOIN',
|
||||
'INNER JOIN',
|
||||
'OUTER JOIN',
|
||||
'LEFT OUTER JOIN',
|
||||
'LEFT JOIN',
|
||||
'RIGHT OUTER JOIN',
|
||||
'RIGHT JOIN',
|
||||
'FULL OUTER JOIN',
|
||||
'FULL JOIN',
|
||||
'NATURAL JOIN',
|
||||
'CROSS JOIN',
|
||||
'SELF JOIN'
|
||||
];
|
||||
|
||||
$type = strtoupper(trim($type));
|
||||
|
||||
// validate join type
|
||||
if (in_array($type, $types) === false) {
|
||||
throw new InvalidArgumentException('Invalid join type ' . $type);
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $type . ' ' . $this->tableName($table) . ' ON ' . $on,
|
||||
'bindings' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the syntax for multiple joins
|
||||
*
|
||||
* @params array $joins
|
||||
* @return array
|
||||
*/
|
||||
public function joins(array $joins = null): array
|
||||
{
|
||||
$query = [];
|
||||
$bindings = [];
|
||||
|
||||
foreach ((array)$joins as $join) {
|
||||
$this->extend($query, $bindings, $this->join($join['type'] ?? 'JOIN', $join['table'] ?? null, $join['on'] ?? null));
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => implode(' ', array_filter($query)),
|
||||
'bindings' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a limit and offset query instruction
|
||||
*
|
||||
* @param integer $offset
|
||||
* @param integer|null $limit
|
||||
* @return array
|
||||
*/
|
||||
public function limit(int $offset = 0, int $limit = null): array
|
||||
{
|
||||
// no need to add it to the query
|
||||
if ($offset === 0 && $limit === null) {
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
$limit = $limit ?? '18446744073709551615';
|
||||
|
||||
$offsetBinding = $this->bindingName('offset');
|
||||
$limitBinding = $this->bindingName('limit');
|
||||
|
||||
return [
|
||||
'query' => 'LIMIT ' . $offsetBinding . ', ' . $limitBinding,
|
||||
'bindings' => [
|
||||
$limitBinding => $limit,
|
||||
$offsetBinding => $offset,
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the order by syntax
|
||||
*
|
||||
* @param string $order
|
||||
* @return array
|
||||
*/
|
||||
public function order(string $order = null): array
|
||||
{
|
||||
if (empty($order) === true) {
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => 'ORDER BY ' . $order,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a query array into a final string
|
||||
*
|
||||
* @param array $query
|
||||
* @param string $separator
|
||||
* @return string
|
||||
*/
|
||||
public function query(array $query, string $separator = ' ')
|
||||
{
|
||||
return implode($separator, array_filter($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes an identifier (table *or* column)
|
||||
* Default version for MySQL
|
||||
*
|
||||
* @param $identifier string
|
||||
* @return string
|
||||
*/
|
||||
public function quoteIdentifier(string $identifier): string
|
||||
{
|
||||
// * is special
|
||||
if ($identifier === '*') {
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
// replace every backtick with two backticks
|
||||
$identifier = str_replace('`', '``', $identifier);
|
||||
|
||||
// wrap in backticks
|
||||
return '`' . $identifier . '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a select clause
|
||||
*
|
||||
* @param array $params List of parameters for the select clause. Check out the defaults for more info.
|
||||
* @return array An array with the query and the bindings
|
||||
*/
|
||||
public function select(array $params = []): array
|
||||
{
|
||||
$defaults = [
|
||||
'table' => '',
|
||||
'columns' => '*',
|
||||
'join' => null,
|
||||
'distinct' => false,
|
||||
'where' => null,
|
||||
'group' => null,
|
||||
'having' => null,
|
||||
'order' => null,
|
||||
'offset' => 0,
|
||||
'limit' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
|
||||
$options = array_merge($defaults, $params);
|
||||
$bindings = $options['bindings'];
|
||||
$query = ['SELECT'];
|
||||
|
||||
// select distinct values
|
||||
if ($options['distinct'] === true) {
|
||||
$query[] = 'DISTINCT';
|
||||
}
|
||||
|
||||
// columns
|
||||
$query[] = $this->selected($options['table'], $options['columns']);
|
||||
|
||||
// from
|
||||
$this->extend($query, $bindings, $this->from($options['table']));
|
||||
|
||||
// joins
|
||||
$this->extend($query, $bindings, $this->joins($options['join']));
|
||||
|
||||
// where
|
||||
$this->extend($query, $bindings, $this->where($options['where']));
|
||||
|
||||
// group
|
||||
$this->extend($query, $bindings, $this->group($options['group']));
|
||||
|
||||
// having
|
||||
$this->extend($query, $bindings, $this->having($options['having']));
|
||||
|
||||
// order
|
||||
$this->extend($query, $bindings, $this->order($options['order']));
|
||||
|
||||
// offset and limit
|
||||
$this->extend($query, $bindings, $this->limit($options['offset'], $options['limit']));
|
||||
|
||||
return [
|
||||
'query' => $this->query($query),
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a columns definition from string or array
|
||||
*
|
||||
* @param string $table
|
||||
* @param array|string|null $columns
|
||||
* @return string
|
||||
*/
|
||||
public function selected($table, $columns = null): string
|
||||
{
|
||||
// all columns
|
||||
if (empty($columns) === true) {
|
||||
return '*';
|
||||
}
|
||||
|
||||
// array of columns
|
||||
if (is_array($columns) === true) {
|
||||
|
||||
// validate columns
|
||||
$result = [];
|
||||
|
||||
foreach ($columns as $column) {
|
||||
list($table, $columnPart) = $this->splitIdentifier($table, $column);
|
||||
|
||||
if ($this->validateColumn($table, $columnPart) === true) {
|
||||
$result[] = $this->combineIdentifier($table, $columnPart);
|
||||
}
|
||||
}
|
||||
|
||||
return implode(', ', $result);
|
||||
} else {
|
||||
return $columns;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a (qualified) identifier into table and column
|
||||
*
|
||||
* @param $table string Default table if the identifier is not qualified
|
||||
* @param $identifier string
|
||||
* @return array
|
||||
*/
|
||||
public function splitIdentifier($table, $identifier): array
|
||||
{
|
||||
// split by dot, but only outside of quotes
|
||||
$parts = preg_split('/(?:`[^`]*`|"[^"]*")(*SKIP)(*F)|\./', $identifier);
|
||||
|
||||
switch (count($parts)) {
|
||||
// non-qualified identifier
|
||||
case 1:
|
||||
return array($table, $this->unquoteIdentifier($parts[0]));
|
||||
|
||||
// qualified identifier
|
||||
case 2:
|
||||
return array($this->unquoteIdentifier($parts[0]), $this->unquoteIdentifier($parts[1]));
|
||||
|
||||
// every other number is an error
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid identifier ' . $identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of tables for a specified database
|
||||
* MySQL version
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function tables(): array
|
||||
{
|
||||
$binding = $this->bindingName('database');
|
||||
|
||||
return [
|
||||
'query' => 'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ' . $binding,
|
||||
'bindings' => [
|
||||
$binding => $this->database->database
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and quotes a table name
|
||||
*
|
||||
* @param string $table
|
||||
* @return string
|
||||
*/
|
||||
public function tableName(string $table): string
|
||||
{
|
||||
// validate table
|
||||
if ($this->database->validateTable($table) === false) {
|
||||
throw new InvalidArgumentException('Invalid table ' . $table);
|
||||
}
|
||||
|
||||
return $this->quoteIdentifier($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unquotes an identifier (table *or* column)
|
||||
*
|
||||
* @param $identifier string
|
||||
* @return string
|
||||
*/
|
||||
public function unquoteIdentifier(string $identifier): string
|
||||
{
|
||||
// remove quotes around the identifier
|
||||
if (in_array(Str::substr($identifier, 0, 1), ['"', '`']) === true) {
|
||||
$identifier = Str::substr($identifier, 1);
|
||||
}
|
||||
|
||||
if (in_array(Str::substr($identifier, -1), ['"', '`']) === true) {
|
||||
$identifier = Str::substr($identifier, 0, -1);
|
||||
}
|
||||
|
||||
// unescape duplicated quotes
|
||||
return str_replace(['""', '``'], ['"', '`'], $identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an update clause
|
||||
*
|
||||
* @param array $params List of parameters for the update clause. See defaults for more info.
|
||||
* @return array
|
||||
*/
|
||||
public function update(array $params = []): array
|
||||
{
|
||||
$defaults = [
|
||||
'table' => null,
|
||||
'values' => null,
|
||||
'where' => null,
|
||||
'bindings' => []
|
||||
];
|
||||
|
||||
$options = array_merge($defaults, $params);
|
||||
$bindings = $options['bindings'];
|
||||
|
||||
// start the query
|
||||
$query = ['UPDATE ' . $this->tableName($options['table']) . ' SET'];
|
||||
|
||||
// add the values
|
||||
$this->extend($query, $bindings, $this->values($options['table'], $options['values']));
|
||||
|
||||
// add the where clause
|
||||
$this->extend($query, $bindings, $this->where($options['where']));
|
||||
|
||||
return [
|
||||
'query' => $this->query($query),
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a given column name in a table
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $column
|
||||
* @return boolean
|
||||
*/
|
||||
public function validateColumn(string $table, string $column): bool
|
||||
{
|
||||
if ($this->database->validateColumn($table, $column) === false) {
|
||||
throw new InvalidArgumentException('Invalid column ' . $column);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a safe list of values for insert, select or update queries
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param mixed $values A value string or array of values
|
||||
* @param string $separator A separator which should be used to join values
|
||||
* @param boolean $set If true builds a set list of values for update clauses
|
||||
* @param boolean $enforceQualified Always use fully qualified column names
|
||||
*/
|
||||
public function values(string $table, $values, string $separator = ', ', bool $set = true, bool $enforceQualified = false): array
|
||||
{
|
||||
if (is_array($values) === false) {
|
||||
return [
|
||||
'query' => $values,
|
||||
'bindings' => []
|
||||
];
|
||||
}
|
||||
|
||||
if ($set === true) {
|
||||
return $this->valueSet($table, $values, $separator, $enforceQualified);
|
||||
} else {
|
||||
return $this->valueList($table, $values, $separator, $enforceQualified);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of fields and values
|
||||
*
|
||||
* @param string $table
|
||||
* @param string|array $values
|
||||
* @param string $separator
|
||||
* @param bool $enforceQualified
|
||||
* @param array
|
||||
*/
|
||||
public function valueList(string $table, $values, string $separator = ',', bool $enforceQualified = false): array
|
||||
{
|
||||
$fields = [];
|
||||
$query = [];
|
||||
$bindings = [];
|
||||
|
||||
foreach ($values as $key => $value) {
|
||||
$fields[] = $this->columnName($table, $key, $enforceQualified);
|
||||
|
||||
if (in_array($value, static::$literals, true) === true) {
|
||||
$query[] = $value ?: 'null';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value) === true) {
|
||||
$value = json_encode($value);
|
||||
}
|
||||
|
||||
// add the binding
|
||||
$bindings[$bindingName = $this->bindingName('value')] = $value;
|
||||
|
||||
// create the query
|
||||
$query[] = $bindingName;
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => '(' . implode($separator, $fields) . ') VALUES (' . implode($separator, $query) . ')',
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a set of values
|
||||
*
|
||||
* @param string $table
|
||||
* @param string|array $values
|
||||
* @param string $separator
|
||||
* @param bool $enforceQualified
|
||||
* @param array
|
||||
*/
|
||||
public function valueSet(string $table, $values, string $separator = ',', bool $enforceQualified = false): array
|
||||
{
|
||||
$query = [];
|
||||
$bindings = [];
|
||||
|
||||
foreach ($values as $column => $value) {
|
||||
$key = $this->columnName($table, $column, $enforceQualified);
|
||||
|
||||
if (in_array($value, static::$literals, true) === true) {
|
||||
$query[] = $key . ' = ' . ($value ?: 'null');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($value) === true) {
|
||||
$value = json_encode($value);
|
||||
}
|
||||
|
||||
// add the binding
|
||||
$bindings[$bindingName = $this->bindingName('value')] = $value;
|
||||
|
||||
// create the query
|
||||
$query[] = $key . ' = ' . $bindingName;
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => implode($separator, $query),
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array|null $where
|
||||
* @param array $bindings
|
||||
* @return array
|
||||
*/
|
||||
public function where($where, array $bindings = []): array
|
||||
{
|
||||
if (empty($where) === true) {
|
||||
return [
|
||||
'query' => null,
|
||||
'bindings' => [],
|
||||
];
|
||||
}
|
||||
|
||||
if (is_string($where) === true) {
|
||||
return [
|
||||
'query' => 'WHERE ' . $where,
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
|
||||
$query = [];
|
||||
|
||||
foreach ($where as $key => $value) {
|
||||
$binding = $this->bindingName('where_' . $key);
|
||||
$bindings[$binding] = $value;
|
||||
|
||||
$query[] = $key . ' = ' . $binding;
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => 'WHERE ' . implode(' AND ', $query),
|
||||
'bindings' => $bindings
|
||||
];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user