first version
This commit is contained in:
585
kirby/src/Toolkit/A.php
Executable file
585
kirby/src/Toolkit/A.php
Executable file
@@ -0,0 +1,585 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A set of handy methods to simplify array handling
|
||||
* and make it more consistent.
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class A
|
||||
{
|
||||
|
||||
/**
|
||||
* Appends the given array
|
||||
*
|
||||
* @param array $array
|
||||
* @param array $append
|
||||
* @return array
|
||||
*/
|
||||
public static function append(array $array, array $append): array
|
||||
{
|
||||
return $array + $append;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an element of an array by key
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* echo A::get($array, 'cat');
|
||||
* // output: 'miao'
|
||||
*
|
||||
* echo A::get($array, 'elephant', 'shut up');
|
||||
* // output: 'shut up'
|
||||
*
|
||||
* $catAndDog = A::get($array, ['cat', 'dog']);
|
||||
* // result: ['cat' => 'miao', 'dog' => 'wuff'];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param mixed $key The key to look for
|
||||
* @param mixed $default Optional default value, which should be
|
||||
* returned if no element has been found
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get(array $array, $key, $default = null)
|
||||
{
|
||||
// return the entire array if the key is null
|
||||
if ($key === null) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
// get an array of keys
|
||||
if (is_array($key) === true) {
|
||||
$result = [];
|
||||
foreach ($key as $k) {
|
||||
$result[$k] = static::get($array, $k, $default);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (isset($array[$key]) === true) {
|
||||
return $array[$key];
|
||||
}
|
||||
|
||||
// support dot notation
|
||||
if (strpos($key, '.') !== false) {
|
||||
$keys = explode('.', $key);
|
||||
|
||||
foreach ($keys as $innerKey) {
|
||||
if (isset($array[$innerKey]) === false) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$array = $array[$innerKey];
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function join($value, $separator = ', ')
|
||||
{
|
||||
if (is_string($value) === true) {
|
||||
return $value;
|
||||
}
|
||||
return implode($separator, $value);
|
||||
}
|
||||
|
||||
const MERGE_OVERWRITE = 0;
|
||||
const MERGE_APPEND = 1;
|
||||
const MERGE_REPLACE = 2;
|
||||
|
||||
/**
|
||||
* Merges arrays recursively
|
||||
*
|
||||
* @param array $array1
|
||||
* @param array $array2
|
||||
* @param boolean $mode Behavior for elements with numeric keys;
|
||||
* A::MERGE_APPEND: elements are appended, keys are reset;
|
||||
* A::MERGE_OVERWRITE: elements are overwritten, keys are preserved
|
||||
* A::MERGE_REPLACE: non-associative arrays are completely replaced
|
||||
* @return array
|
||||
*/
|
||||
public static function merge($array1, $array2, $mode = A::MERGE_APPEND)
|
||||
{
|
||||
$merged = $array1;
|
||||
|
||||
if (static::isAssociative($array1) === false && $mode === static::MERGE_REPLACE) {
|
||||
return $array2;
|
||||
}
|
||||
|
||||
foreach ($array2 as $key => $value) {
|
||||
|
||||
// append to the merged array, don't overwrite numeric keys
|
||||
if (is_int($key) === true && $mode == static::MERGE_APPEND) {
|
||||
$merged[] = $value;
|
||||
|
||||
// recursively merge the two array values
|
||||
} elseif (is_array($value) === true && isset($merged[$key]) === true && is_array($merged[$key]) === true) {
|
||||
$merged[$key] = static::merge($merged[$key], $value, $mode);
|
||||
|
||||
// simply overwrite with the value from the second array
|
||||
} else {
|
||||
$merged[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ($mode == static::MERGE_APPEND) {
|
||||
// the keys don't make sense anymore, reset them
|
||||
// array_merge() is the simplest way to renumber
|
||||
// arrays that have both numeric and string keys;
|
||||
// besides the keys, nothing changes here
|
||||
$merged = array_merge($merged, []);
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plucks a single column from an array
|
||||
*
|
||||
* <code>
|
||||
* $array[] = [
|
||||
* 'id' => 1,
|
||||
* 'username' => 'homer',
|
||||
* ];
|
||||
*
|
||||
* $array[] = [
|
||||
* 'id' => 2,
|
||||
* 'username' => 'marge',
|
||||
* ];
|
||||
*
|
||||
* $array[] = [
|
||||
* 'id' => 3,
|
||||
* 'username' => 'lisa',
|
||||
* ];
|
||||
*
|
||||
* var_dump(A::pluck($array, 'username'));
|
||||
* // result: ['homer', 'marge', 'lisa'];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param string $key The key name of the column to extract
|
||||
* @return array The result array with all values
|
||||
* from that column.
|
||||
*/
|
||||
public static function pluck(array $array, string $key)
|
||||
{
|
||||
$output = [];
|
||||
foreach ($array as $a) {
|
||||
if (isset($a[$key]) === true) {
|
||||
$output[] = $a[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends the given array
|
||||
*
|
||||
* @param array $array
|
||||
* @param array $prepend
|
||||
* @return array
|
||||
*/
|
||||
public static function prepend(array $array, array $prepend): array
|
||||
{
|
||||
return $prepend + $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles an array and keeps the keys
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $shuffled = A::shuffle($array);
|
||||
* // output: [
|
||||
* // 'dog' => 'wuff',
|
||||
* // 'cat' => 'miao',
|
||||
* // 'bird' => 'tweet'
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return array The shuffled result array
|
||||
*/
|
||||
public static function shuffle(array $array): array
|
||||
{
|
||||
$keys = array_keys($array);
|
||||
$new = [];
|
||||
|
||||
shuffle($keys);
|
||||
|
||||
// resort the array
|
||||
foreach ($keys as $key) {
|
||||
$new[$key] = $array[$key];
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element of an array
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $first = A::first($array);
|
||||
* // first: 'miao'
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return mixed The first element
|
||||
*/
|
||||
public static function first(array $array)
|
||||
{
|
||||
return array_shift($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last element of an array
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $last = A::last($array);
|
||||
* // last: 'tweet'
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return mixed The last element
|
||||
*/
|
||||
public static function last(array $array)
|
||||
{
|
||||
return array_pop($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an array up with additional elements to certain amount.
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $result = A::fill($array, 5, 'elephant');
|
||||
*
|
||||
* // result: [
|
||||
* // 'cat',
|
||||
* // 'dog',
|
||||
* // 'bird',
|
||||
* // 'elephant',
|
||||
* // 'elephant',
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param int $limit The number of elements the array should
|
||||
* contain after filling it up.
|
||||
* @param mixed $fill The element, which should be used to
|
||||
* fill the array
|
||||
* @return array The filled-up result array
|
||||
*/
|
||||
public static function fill(array $array, int $limit, $fill = 'placeholder'): array
|
||||
{
|
||||
if (count($array) < $limit) {
|
||||
$diff = $limit - count($array);
|
||||
for ($x = 0; $x < $diff; $x++) {
|
||||
$array[] = $fill;
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an array item to a new index
|
||||
*
|
||||
* @param array $array
|
||||
* @param int $from
|
||||
* @param int $to
|
||||
* @return array
|
||||
*/
|
||||
public static function move(array $array, int $from, int $to): array
|
||||
{
|
||||
$total = count($array);
|
||||
|
||||
if ($from >= $total || $from < 0) {
|
||||
throw new Exception('Invalid "from" index');
|
||||
}
|
||||
|
||||
if ($to >= $total || $to < 0) {
|
||||
throw new Exception('Invalid "to" index');
|
||||
}
|
||||
|
||||
// remove the item from the array
|
||||
$item = array_splice($array, $from, 1);
|
||||
|
||||
// inject it at the new position
|
||||
array_splice($array, $to, 0, $item);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for missing elements in an array
|
||||
*
|
||||
* This is very handy to check for missing
|
||||
* user values in a request for example.
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $required = ['cat', 'elephant'];
|
||||
*
|
||||
* $missng = A::missing($array, $required);
|
||||
* // missing: [
|
||||
* // 'elephant'
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param array $required An array of required keys
|
||||
* @return array An array of missing fields. If this
|
||||
* is empty, nothing is missing.
|
||||
*/
|
||||
public static function missing(array $array, array $required = []): array
|
||||
{
|
||||
$missing = [];
|
||||
foreach ($required as $r) {
|
||||
if (isset($array[$r]) === false) {
|
||||
$missing[] = $r;
|
||||
}
|
||||
}
|
||||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts a multi-dimensional array by a certain column
|
||||
*
|
||||
* <code>
|
||||
* $array[0] = [
|
||||
* 'id' => 1,
|
||||
* 'username' => 'mike',
|
||||
* ];
|
||||
*
|
||||
* $array[1] = [
|
||||
* 'id' => 2,
|
||||
* 'username' => 'peter',
|
||||
* ];
|
||||
*
|
||||
* $array[3] = [
|
||||
* 'id' => 3,
|
||||
* 'username' => 'john',
|
||||
* ];
|
||||
*
|
||||
* $sorted = A::sort($array, 'username ASC');
|
||||
* // Array
|
||||
* // (
|
||||
* // [0] => Array
|
||||
* // (
|
||||
* // [id] => 3
|
||||
* // [username] => john
|
||||
* // )
|
||||
* // [1] => Array
|
||||
* // (
|
||||
* // [id] => 1
|
||||
* // [username] => mike
|
||||
* // )
|
||||
* // [2] => Array
|
||||
* // (
|
||||
* // [id] => 2
|
||||
* // [username] => peter
|
||||
* // )
|
||||
* // )
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param string $field The name of the column
|
||||
* @param string $direction desc (descending) or asc (ascending)
|
||||
* @param int $method A PHP sort method flag or 'natural' for
|
||||
* natural sorting, which is not supported in
|
||||
* PHP by sort flags
|
||||
* @return array The sorted array
|
||||
*/
|
||||
public static function sort(array $array, string $field, string $direction = 'desc', $method = SORT_REGULAR): array
|
||||
{
|
||||
$direction = strtolower($direction) == 'desc' ? SORT_DESC : SORT_ASC;
|
||||
$helper = [];
|
||||
$result = [];
|
||||
|
||||
// build the helper array
|
||||
foreach ($array as $key => $row) {
|
||||
$helper[$key] = $row[$field];
|
||||
}
|
||||
|
||||
// natural sorting
|
||||
if ($direction === SORT_DESC) {
|
||||
arsort($helper, $method);
|
||||
} else {
|
||||
asort($helper, $method);
|
||||
}
|
||||
|
||||
// rebuild the original array
|
||||
foreach ($helper as $key => $val) {
|
||||
$result[$key] = $array[$key];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks wether an array is associative or not
|
||||
*
|
||||
* <code>
|
||||
* $array = ['a', 'b', 'c'];
|
||||
*
|
||||
* A::isAssociative($array);
|
||||
* // returns: false
|
||||
*
|
||||
* $array = ['a' => 'a', 'b' => 'b', 'c' => 'c'];
|
||||
*
|
||||
* A::isAssociative($array);
|
||||
* // returns: true
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The array to analyze
|
||||
* @return boolean true: The array is associative false: It's not
|
||||
*/
|
||||
public static function isAssociative(array $array): bool
|
||||
{
|
||||
return ctype_digit(implode(null, array_keys($array))) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the average value of an array
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param int $decimals The number of decimals to return
|
||||
* @return float The average value
|
||||
*/
|
||||
public static function average(array $array, int $decimals = 0): float
|
||||
{
|
||||
return round((array_sum($array) / sizeof($array)), $decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges arrays recursively
|
||||
*
|
||||
* <code>
|
||||
* $defaults = [
|
||||
* 'username' => 'admin',
|
||||
* 'password' => 'admin',
|
||||
* ];
|
||||
*
|
||||
* $options = A::extend($defaults, ['password' => 'super-secret']);
|
||||
* // returns: [
|
||||
* // 'username' => 'admin',
|
||||
* // 'password' => 'super-secret'
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function extend(...$arrays): array
|
||||
{
|
||||
return array_merge_recursive(...$arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an array with a second array
|
||||
* The second array can contain callbacks as values,
|
||||
* which will get the original values as argument
|
||||
*
|
||||
* <code>
|
||||
* $user = [
|
||||
* 'username' => 'homer',
|
||||
* 'email' => 'homer@simpsons.com'
|
||||
* ];
|
||||
*
|
||||
* // simple updates
|
||||
* A::update($user, [
|
||||
* 'username' => 'homer j. simpson'
|
||||
* ]);
|
||||
*
|
||||
* // with callback
|
||||
* A::update($user, [
|
||||
* 'username' => function ($username) {
|
||||
* return $username . ' j. simpson'
|
||||
* }
|
||||
* ]);
|
||||
* </code>
|
||||
*
|
||||
* @param array $array
|
||||
* @param array $update
|
||||
* @return array
|
||||
*/
|
||||
public static function update(array $array, array $update): array
|
||||
{
|
||||
foreach ($update as $key => $value) {
|
||||
if (is_a($value, 'Closure') === true) {
|
||||
$array[$key] = call_user_func($value, static::get($array, $key));
|
||||
} else {
|
||||
$array[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given value in an array
|
||||
* if it's not an array yet.
|
||||
*
|
||||
* @param mixed|null $array
|
||||
* @return array
|
||||
*/
|
||||
public static function wrap($array = null): array
|
||||
{
|
||||
if ($array === null) {
|
||||
return [];
|
||||
} elseif (is_array($array) === false) {
|
||||
return [$array];
|
||||
} else {
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
}
|
1176
kirby/src/Toolkit/Collection.php
Executable file
1176
kirby/src/Toolkit/Collection.php
Executable file
File diff suppressed because it is too large
Load Diff
279
kirby/src/Toolkit/Component.php
Executable file
279
kirby/src/Toolkit/Component.php
Executable file
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use ArgumentCountError;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use TypeError;
|
||||
|
||||
/**
|
||||
* Vue-like components
|
||||
*/
|
||||
class Component
|
||||
{
|
||||
|
||||
/**
|
||||
* Registry for all component mixins
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $mixins = [];
|
||||
|
||||
/**
|
||||
* Registry for all component types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $types = [];
|
||||
|
||||
/**
|
||||
* An array of all passed attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attrs = [];
|
||||
|
||||
/**
|
||||
* An array of all computed properties
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $computed = [];
|
||||
|
||||
/**
|
||||
* An array of all registered methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $methods = [];
|
||||
|
||||
/**
|
||||
* An array of all component options
|
||||
* from the component definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* An array of all resolved props
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $props = [];
|
||||
|
||||
/**
|
||||
* The component type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* Magic caller for defined methods and properties
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $name, array $arguments = [])
|
||||
{
|
||||
if (array_key_exists($name, $this->computed) === true) {
|
||||
return $this->computed[$name];
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $this->props) === true) {
|
||||
return $this->props[$name];
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $this->methods) === true) {
|
||||
return $this->methods[$name]->call($this, ...$arguments);
|
||||
}
|
||||
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new component for the given type
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $attrs
|
||||
*/
|
||||
public function __construct(string $type, array $attrs = [])
|
||||
{
|
||||
if (isset(static::$types[$type]) === false) {
|
||||
throw new InvalidArgumentException('Undefined component type: ' . $type);
|
||||
}
|
||||
|
||||
$this->attrs = $attrs;
|
||||
$this->options = $options = $this->setup($type);
|
||||
$this->methods = $methods = $options['methods'] ?? [];
|
||||
|
||||
foreach ($attrs as $attrName => $attrValue) {
|
||||
$this->$attrName = $attrValue;
|
||||
}
|
||||
|
||||
if (isset($options['props']) === true) {
|
||||
$this->applyProps($options['props']);
|
||||
}
|
||||
|
||||
if (isset($options['computed']) === true) {
|
||||
$this->applyComputed($options['computed']);
|
||||
}
|
||||
|
||||
$this->attrs = $attrs;
|
||||
$this->methods = $methods;
|
||||
$this->options = $options;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved var_dump output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debuginfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for missing properties to return
|
||||
* null instead of an error
|
||||
*
|
||||
* @param string $attr
|
||||
* @return null
|
||||
*/
|
||||
public function __get(string $attr)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of default options for each component.
|
||||
* This can be overwritten by extended classes
|
||||
* to define basic options that should always
|
||||
* be applied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function defaults(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all defined props and apply the
|
||||
* passed values.
|
||||
*
|
||||
* @param array $props
|
||||
* @return void
|
||||
*/
|
||||
protected function applyProps(array $props): void
|
||||
{
|
||||
foreach ($props as $propName => $propFunction) {
|
||||
if (is_callable($propFunction) === true) {
|
||||
if (isset($this->attrs[$propName]) === true) {
|
||||
try {
|
||||
$this->$propName = $this->props[$propName] = $propFunction->call($this, $this->attrs[$propName]);
|
||||
} catch (TypeError $e) {
|
||||
throw new TypeError('Invalid value for "' . $propName . '"');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$this->$propName = $this->props[$propName] = $propFunction->call($this);
|
||||
} catch (ArgumentCountError $e) {
|
||||
throw new ArgumentCountError('Please provide a value for "' . $propName . '"');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->$propName = $this->props[$propName] = $propFunction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all computed properties and calculate their values.
|
||||
* This must happen after all props are registered.
|
||||
*
|
||||
* @param array $computed
|
||||
* @return void
|
||||
*/
|
||||
protected function applyComputed(array $computed): void
|
||||
{
|
||||
foreach ($computed as $computedName => $computedFunction) {
|
||||
$this->$computedName = $this->computed[$computedName] = $computedFunction->call($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a component definition by type
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function load(string $type): array
|
||||
{
|
||||
$definition = static::$types[$type];
|
||||
|
||||
// load definitions from string
|
||||
if (is_array($definition) === false) {
|
||||
static::$types[$type] = $definition = include $definition;
|
||||
}
|
||||
|
||||
return $definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all options from the component definition
|
||||
* mixes in the defaults from the defaults method and
|
||||
* then injects all additional mixins, defined in the
|
||||
* component options.
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function setup(string $type): array
|
||||
{
|
||||
// load component definition
|
||||
$definition = static::load($type);
|
||||
|
||||
if (isset($definition['extends']) === true) {
|
||||
// extend other definitions
|
||||
$options = array_replace_recursive(static::defaults(), static::load($definition['extends']), $definition);
|
||||
} else {
|
||||
// inject defaults
|
||||
$options = array_replace_recursive(static::defaults(), $definition);
|
||||
}
|
||||
|
||||
// inject mixins
|
||||
if (isset($options['mixins']) === true) {
|
||||
foreach ($options['mixins'] as $mixin) {
|
||||
if (isset(static::$mixins[$mixin]) === true) {
|
||||
$options = array_replace_recursive(static::$mixins[$mixin], $options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all props and computed props to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
if (is_a($this->options['toArray'] ?? null, 'Closure') === true) {
|
||||
return $this->options['toArray']->call($this);
|
||||
}
|
||||
|
||||
$array = array_merge($this->attrs, $this->props, $this->computed);
|
||||
|
||||
ksort($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
21
kirby/src/Toolkit/Config.php
Executable file
21
kirby/src/Toolkit/Config.php
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
/**
|
||||
* This is the core class to handle
|
||||
* configuration values/constants.
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
class Config extends Silo
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $data = [];
|
||||
}
|
66
kirby/src/Toolkit/Controller.php
Executable file
66
kirby/src/Toolkit/Controller.php
Executable file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use ReflectionFunction;
|
||||
|
||||
/**
|
||||
* A smart extension of Closures with
|
||||
* magic dependency injection based on the
|
||||
* defined variable names.
|
||||
*/
|
||||
class Controller
|
||||
{
|
||||
protected $function;
|
||||
|
||||
public function __construct(Closure $function)
|
||||
{
|
||||
$this->function = $function;
|
||||
}
|
||||
|
||||
public function arguments(array $data = []): array
|
||||
{
|
||||
$info = new ReflectionFunction($this->function);
|
||||
$args = [];
|
||||
|
||||
foreach ($info->getParameters() as $parameter) {
|
||||
$name = $parameter->getName();
|
||||
|
||||
if (isset($data[$name]) === false) {
|
||||
throw new Exception(sprintf('The "%s" parameter is missing', $name));
|
||||
}
|
||||
|
||||
$args[] = $data[$name];
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
public function call($bind = null, $data = [])
|
||||
{
|
||||
$args = $this->arguments($data);
|
||||
|
||||
if ($bind === null) {
|
||||
return call_user_func($this->function, ...$args);
|
||||
}
|
||||
|
||||
return $this->function->call($bind, ...$args);
|
||||
}
|
||||
|
||||
public static function load(string $file)
|
||||
{
|
||||
if (file_exists($file) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$function = require $file;
|
||||
|
||||
if (is_a($function, 'Closure') === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new static($function);
|
||||
}
|
||||
}
|
401
kirby/src/Toolkit/Dir.php
Executable file
401
kirby/src/Toolkit/Dir.php
Executable file
@@ -0,0 +1,401 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Low level directory handling utilities
|
||||
*/
|
||||
class Dir
|
||||
{
|
||||
|
||||
/**
|
||||
* Ignore when scanning directories
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $ignore = [
|
||||
'.',
|
||||
'..',
|
||||
'.DS_Store',
|
||||
'.gitignore',
|
||||
'.git',
|
||||
'.svn',
|
||||
'.htaccess',
|
||||
'Thumb.db',
|
||||
'@eaDir'
|
||||
];
|
||||
|
||||
/**
|
||||
* Copy the directory to a new destination
|
||||
*
|
||||
* @param string $dir
|
||||
* @param string $target
|
||||
* @return bool
|
||||
*/
|
||||
public static function copy(string $dir, string $target): bool
|
||||
{
|
||||
if (is_dir($dir) === false) {
|
||||
throw new Exception('The directory "' . $dir . '" does not exist');
|
||||
}
|
||||
|
||||
if (is_dir($target) === true) {
|
||||
throw new Exception('The target directory "' . $target . '" exists');
|
||||
}
|
||||
|
||||
if (static::make($target) !== true) {
|
||||
throw new Exception('The target directory "' . $target . '" could not be created');
|
||||
}
|
||||
|
||||
foreach (static::read($dir) as $name) {
|
||||
$root = $dir . '/' . $name;
|
||||
|
||||
if (is_dir($root) === true) {
|
||||
static::copy($root, $target . '/' . $name);
|
||||
} else {
|
||||
F::copy($root, $target . '/' . $name);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all subdirectories
|
||||
*
|
||||
* @param string $dir
|
||||
* @param bool $absolute
|
||||
* @return array
|
||||
*/
|
||||
public static function dirs(string $dir, array $ignore = null, bool $absolute = false): array
|
||||
{
|
||||
$result = array_values(array_filter(static::read($dir, $ignore, true), 'is_dir'));
|
||||
|
||||
if ($absolute !== true) {
|
||||
$result = array_map('basename', $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files
|
||||
*
|
||||
* @param string $dir
|
||||
* @param bool $absolute
|
||||
* @return array
|
||||
*/
|
||||
public static function files(string $dir, array $ignore = null, bool $absolute = false): array
|
||||
{
|
||||
$result = array_values(array_filter(static::read($dir, $ignore, true), 'is_file'));
|
||||
|
||||
if ($absolute !== true) {
|
||||
$result = array_map('basename', $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the directory and all subdirectories
|
||||
*
|
||||
* @param string $dir
|
||||
* @param array $ignore
|
||||
* @return array
|
||||
*/
|
||||
public static function index(string $dir, bool $recursive = false, array $ignore = null, string $path = null)
|
||||
{
|
||||
$result = [];
|
||||
$dir = realpath($dir);
|
||||
$items = static::read($dir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$root = $dir . '/' . $item;
|
||||
$entry = $path !== null ? $path . '/' . $item: $item;
|
||||
$result[] = $entry;
|
||||
|
||||
if ($recursive === true && is_dir($root) === true) {
|
||||
$result = array_merge($result, static::index($root, true, $ignore, $entry));
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the folder has any contents
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isEmpty(string $dir): bool
|
||||
{
|
||||
return count(static::read($dir)) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the directory is readable
|
||||
*
|
||||
* @param string $dir
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isReadable(string $dir): bool
|
||||
{
|
||||
return is_readable($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the directory is writable
|
||||
*
|
||||
* @param string $dir
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isWritable(string $dir): bool
|
||||
{
|
||||
return is_writable($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a (symbolic) link to a directory
|
||||
*
|
||||
* @param string $source
|
||||
* @param string $link
|
||||
* @return boolean
|
||||
*/
|
||||
public static function link(string $source, string $link): bool
|
||||
{
|
||||
Dir::make(dirname($link), true);
|
||||
|
||||
if (is_dir($link) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_dir($source) === false) {
|
||||
throw new Exception(sprintf('The directory "%s" does not exist and cannot be linked', $source));
|
||||
}
|
||||
|
||||
return symlink($source, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new directory
|
||||
*
|
||||
* @param string $dir The path for the new directory
|
||||
* @param boolean $recursive Create all parent directories, which don't exist
|
||||
* @return boolean True: the dir has been created, false: creating failed
|
||||
*/
|
||||
public static function make(string $dir, bool $recursive = true): bool
|
||||
{
|
||||
if (empty($dir) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_dir($dir) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$parent = dirname($dir);
|
||||
|
||||
if ($recursive === true) {
|
||||
if (is_dir($parent) === false) {
|
||||
static::make($parent, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_writable($parent) === false) {
|
||||
throw new Exception(sprintf('The directory "%s" cannot be created', $dir));
|
||||
}
|
||||
|
||||
return mkdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively check when the dir and all
|
||||
* subfolders have been modified for the last time.
|
||||
*
|
||||
* @param string $dir The path of the directory
|
||||
* @param string $format
|
||||
* @param string $handler
|
||||
* @return int
|
||||
*/
|
||||
public static function modified(string $dir, string $format = null, string $handler = 'date')
|
||||
{
|
||||
$modified = filemtime($dir);
|
||||
$items = static::read($dir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if (is_file($dir . '/' . $item) === true) {
|
||||
$newModified = filemtime($dir . '/' . $item);
|
||||
} else {
|
||||
$newModified = static::modified($dir . '/' . $item);
|
||||
}
|
||||
|
||||
$modified = ($newModified > $modified) ? $newModified : $modified;
|
||||
}
|
||||
|
||||
return $format !== null ? $handler($format, $modified) : $modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a directory to a new location
|
||||
*
|
||||
* @param string $old The current path of the directory
|
||||
* @param string $new The desired path where the dir should be moved to
|
||||
* @return boolean true: the directory has been moved, false: moving failed
|
||||
*/
|
||||
public static function move(string $old, string $new): bool
|
||||
{
|
||||
if ($old === $new) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_dir($old) === false || is_dir($new) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (static::make(dirname($new), true) !== true) {
|
||||
throw new Exception('The parent directory cannot be created');
|
||||
}
|
||||
|
||||
return rename($old, $new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nicely formatted size of all the contents of the folder
|
||||
*
|
||||
* @param string $dir The path of the directory
|
||||
* @return mixed
|
||||
*/
|
||||
public static function niceSize(string $dir)
|
||||
{
|
||||
return F::niceSize(static::size($dir));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all files from a directory and returns them as an array.
|
||||
* It skips unwanted invisible stuff.
|
||||
*
|
||||
* @param string $dir The path of directory
|
||||
* @param array $ignore Optional array with filenames, which should be ignored
|
||||
* @param bool $absolute If true, the full path for each item will be returned
|
||||
* @return array An array of filenames
|
||||
*/
|
||||
public static function read(string $dir, array $ignore = null, bool $absolute = false): array
|
||||
{
|
||||
if (is_dir($dir) === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// create the ignore pattern
|
||||
$ignore = $ignore ?? static::$ignore;
|
||||
$ignore = array_merge($ignore, ['.', '..']);
|
||||
|
||||
// scan for all files and dirs
|
||||
$result = array_values((array)array_diff(scandir($dir), $ignore));
|
||||
|
||||
// add absolute paths
|
||||
if ($absolute === true) {
|
||||
$result = array_map(function ($item) use ($dir) {
|
||||
return $dir . '/' . $item;
|
||||
}, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a folder including all containing files and folders
|
||||
*
|
||||
* @param string $dir
|
||||
* @return boolean
|
||||
*/
|
||||
public static function remove(string $dir): bool
|
||||
{
|
||||
$dir = realpath($dir);
|
||||
|
||||
if (is_dir($dir) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_link($dir) === true) {
|
||||
return unlink($dir);
|
||||
}
|
||||
|
||||
foreach (scandir($dir) as $childName) {
|
||||
if (in_array($childName, ['.', '..']) === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$child = $dir . '/' . $childName;
|
||||
|
||||
if (is_link($child) === true) {
|
||||
unlink($child);
|
||||
} elseif (is_dir($child) === true) {
|
||||
static::remove($child);
|
||||
} else {
|
||||
F::remove($child);
|
||||
}
|
||||
}
|
||||
|
||||
return rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of the directory and all subfolders and files
|
||||
*
|
||||
* @param string $dir The path of the directory
|
||||
* @return mixed
|
||||
*/
|
||||
public static function size(string $dir)
|
||||
{
|
||||
if (is_dir($dir) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$size = 0;
|
||||
$items = static::read($dir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
$root = $dir . '/' . $item;
|
||||
|
||||
if (is_dir($root) === true) {
|
||||
$size += static::size($root);
|
||||
} elseif (is_file($root) === true) {
|
||||
$size += F::size($root);
|
||||
}
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the directory or any subdirectory has been
|
||||
* modified after the given timestamp
|
||||
*
|
||||
* @param string $dir
|
||||
* @param int $time
|
||||
* @return bool
|
||||
*/
|
||||
public static function wasModifiedAfter(string $dir, int $time): bool
|
||||
{
|
||||
if (filemtime($dir) > $time) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$content = static::read($dir);
|
||||
|
||||
foreach ($content as $item) {
|
||||
$subdir = $dir . '/' . $item;
|
||||
|
||||
if (filemtime($subdir) > $time) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_dir($subdir) === true && static::wasModifiedAfter($subdir, $time) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
134
kirby/src/Toolkit/Escape.php
Executable file
134
kirby/src/Toolkit/Escape.php
Executable file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Zend\Escaper\Escaper;
|
||||
|
||||
/**
|
||||
* Wrapper for the Zend Escaper
|
||||
*
|
||||
* @link https://github.com/zendframework/zend-escaper
|
||||
*/
|
||||
class Escape
|
||||
{
|
||||
|
||||
/**
|
||||
* Escape common HTML attributes data
|
||||
*
|
||||
* This can be used to put untrusted data into typical attribute values
|
||||
* like width, name, value, etc.
|
||||
*
|
||||
* This should not be used for complex attributes like href, src, style,
|
||||
* or any of the event handlers like onmouseover.
|
||||
* Use esc($string, 'js') for event handler attributes, esc($string, 'url')
|
||||
* for src attributes and esc($string, 'css') for style attributes.
|
||||
*
|
||||
* <div attr=...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...>content</div>
|
||||
* <div attr='...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...'>content</div>
|
||||
* <div attr="...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...">content</div>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function attr($string)
|
||||
{
|
||||
return (new Escaper('utf-8'))->escapeHtmlAttr($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML style property values
|
||||
*
|
||||
* This can be used to put untrusted data into a stylesheet or a style tag.
|
||||
*
|
||||
* Stay away from putting untrusted data into complex properties like url,
|
||||
* behavior, and custom (-moz-binding). You should also not put untrusted data
|
||||
* into IE’s expression property value which allows JavaScript.
|
||||
*
|
||||
* <style>selector { property : ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...; } </style>
|
||||
* <style>selector { property : "...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE..."; } </style>
|
||||
* <span style="property : ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...">text</span>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function css($string)
|
||||
{
|
||||
return (new Escaper('utf-8'))->escapeCss($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML element content
|
||||
*
|
||||
* This can be used to put untrusted data directly into the HTML body somewhere.
|
||||
* This includes inside normal tags like div, p, b, td, etc.
|
||||
*
|
||||
* Escapes &, <, >, ", and ' with HTML entity encoding to prevent switching
|
||||
* into any execution context, such as script, style, or event handlers.
|
||||
*
|
||||
* <body>...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...</body>
|
||||
* <div>...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...</div>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function html($string)
|
||||
{
|
||||
return (new Escaper('utf-8'))->escapeHtml($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape JavaScript data values
|
||||
*
|
||||
* This can be used to put dynamically generated JavaScript code
|
||||
* into both script blocks and event-handler attributes.
|
||||
*
|
||||
* <script>alert('...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...')</script>
|
||||
* <script>x='...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...'</script>
|
||||
* <div onmouseover="x='...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...'"</div>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function js($string)
|
||||
{
|
||||
return (new Escaper('utf-8'))->escapeJs($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape URL parameter values
|
||||
*
|
||||
* This can be used to put untrusted data into HTTP GET parameter values.
|
||||
* This should not be used to escape an entire URI.
|
||||
*
|
||||
* <a href="http://www.somesite.com?test=...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...">link</a>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function url($string)
|
||||
{
|
||||
return rawurlencode($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape XML element content
|
||||
*
|
||||
* Removes offending characters that could be wrongfully interpreted as XML markup.
|
||||
*
|
||||
* The following characters are reserved in XML and will be replaced with their
|
||||
* corresponding XML entities:
|
||||
*
|
||||
* ' is replaced with '
|
||||
* " is replaced with "
|
||||
* & is replaced with &
|
||||
* < is replaced with <
|
||||
* > is replaced with >
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function xml($string)
|
||||
{
|
||||
return htmlspecialchars($string, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||
}
|
||||
}
|
738
kirby/src/Toolkit/F.php
Executable file
738
kirby/src/Toolkit/F.php
Executable file
@@ -0,0 +1,738 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
use Kirby\Http\Header;
|
||||
use ZipArchive;
|
||||
|
||||
/**
|
||||
* Low level file handling utilities
|
||||
*/
|
||||
class F
|
||||
{
|
||||
public static $types = [
|
||||
'archive' => [
|
||||
'gz',
|
||||
'gzip',
|
||||
'tar',
|
||||
'tgz',
|
||||
'zip',
|
||||
],
|
||||
'audio' => [
|
||||
'aif',
|
||||
'aiff',
|
||||
'm4a',
|
||||
'midi',
|
||||
'mp3',
|
||||
'wav',
|
||||
],
|
||||
'code' => [
|
||||
'css',
|
||||
'js',
|
||||
'json',
|
||||
'java',
|
||||
'htm',
|
||||
'html',
|
||||
'php',
|
||||
'rb',
|
||||
'py',
|
||||
'scss',
|
||||
'xml',
|
||||
'yml',
|
||||
],
|
||||
'document' => [
|
||||
'csv',
|
||||
'doc',
|
||||
'docx',
|
||||
'dotx',
|
||||
'indd',
|
||||
'md',
|
||||
'mdown',
|
||||
'pdf',
|
||||
'ppt',
|
||||
'pptx',
|
||||
'rtf',
|
||||
'txt',
|
||||
'xl',
|
||||
'xls',
|
||||
'xlsx',
|
||||
'xltx',
|
||||
],
|
||||
'image' => [
|
||||
'ai',
|
||||
'bmp',
|
||||
'gif',
|
||||
'eps',
|
||||
'ico',
|
||||
'jpeg',
|
||||
'jpg',
|
||||
'jpe',
|
||||
'png',
|
||||
'ps',
|
||||
'psd',
|
||||
'svg',
|
||||
'tif',
|
||||
'tiff',
|
||||
'webp'
|
||||
],
|
||||
'video' => [
|
||||
'avi',
|
||||
'flv',
|
||||
'm4v',
|
||||
'mov',
|
||||
'movie',
|
||||
'mpe',
|
||||
'mpg',
|
||||
'mp4',
|
||||
'ogg',
|
||||
'ogv',
|
||||
'swf',
|
||||
'webm',
|
||||
],
|
||||
];
|
||||
|
||||
public static $units = ['B','kB','MB','GB','TB','PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
/**
|
||||
* Appends new content to an existing file
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
* @param mixed $content Either a string or an array. Arrays will be converted to JSON.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function append(string $file, $content): bool
|
||||
{
|
||||
return static::write($file, $content, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file content as base64 encoded string
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
* @return string
|
||||
*/
|
||||
public static function base64(string $file): string
|
||||
{
|
||||
return base64_encode(static::read($file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file to a new location.
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $target
|
||||
* @param boolean $force
|
||||
* @return boolean
|
||||
*/
|
||||
public static function copy(string $source, string $target, bool $force = false): bool
|
||||
{
|
||||
if (file_exists($source) === false || (file_exists($target) === true && $force === false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$directory = dirname($target);
|
||||
|
||||
// create the parent directory if it does not exist
|
||||
if (is_dir($directory) === false) {
|
||||
Dir::make($directory, true);
|
||||
}
|
||||
|
||||
return copy($source, $target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just an alternative for dirname() to stay consistent
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* $dirname = F::dirname('/var/www/test.txt');
|
||||
* // dirname is /var/www
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $file The path
|
||||
* @return string
|
||||
*/
|
||||
public static function dirname(string $file): string
|
||||
{
|
||||
return dirname($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file exists on disk
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $in
|
||||
* @return boolean
|
||||
*/
|
||||
public static function exists(string $file, string $in = null): bool
|
||||
{
|
||||
try {
|
||||
static::realpath($file, $in);
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension of a file
|
||||
*
|
||||
* @param string $file The filename or path
|
||||
* @param string $extension Set an optional extension to overwrite the current one
|
||||
* @return string
|
||||
*/
|
||||
public static function extension(string $file = null, string $extension = null): string
|
||||
{
|
||||
// overwrite the current extension
|
||||
if ($extension !== null) {
|
||||
return static::name($file) . '.' . $extension;
|
||||
}
|
||||
|
||||
// return the current extension
|
||||
return Str::lower(pathinfo($file, PATHINFO_EXTENSION));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a file extension to a mime type
|
||||
*
|
||||
* @param string $extension
|
||||
* @return string|false
|
||||
*/
|
||||
public static function extensionToMime(string $extension)
|
||||
{
|
||||
return Mime::fromExtension($extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file type for a passed extension
|
||||
*
|
||||
* @param string $extension
|
||||
* @return string|false
|
||||
*/
|
||||
public static function extensionToType(string $extension)
|
||||
{
|
||||
foreach (static::$types as $type => $extensions) {
|
||||
if (in_array($extension, $extensions) === true) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all extensions for a certain file type
|
||||
*
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function extensions(string $type = null)
|
||||
{
|
||||
if ($type === null) {
|
||||
return array_keys(Mime::types());
|
||||
}
|
||||
|
||||
return static::$types[$type] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the filename from a file path
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* $filename = F::filename('/var/www/test.txt');
|
||||
* // filename is test.txt
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $name The path
|
||||
* @return string
|
||||
*/
|
||||
public static function filename(string $name): string
|
||||
{
|
||||
return pathinfo($name, PATHINFO_BASENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file is of a certain type
|
||||
*
|
||||
* @param string $file Full path to the file
|
||||
* @param string $value An extension or mime type
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is(string $file, string $value): bool
|
||||
{
|
||||
// check for the extension
|
||||
if (in_array($value, static::extensions()) === true) {
|
||||
return static::extension($file) === $value;
|
||||
}
|
||||
|
||||
// check for the mime type
|
||||
if (strpos($value, '/') !== false) {
|
||||
return static::mime($file) === $value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is readable
|
||||
*
|
||||
* @param string $file
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isReadable(string $file): bool
|
||||
{
|
||||
return is_readable($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is writable
|
||||
*
|
||||
* @param string $file
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isWritable(string $file): bool
|
||||
{
|
||||
if (file_exists($file) === false) {
|
||||
return is_writable(dirname($file));
|
||||
}
|
||||
|
||||
return is_writable($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a (symbolic) link to a file
|
||||
*
|
||||
* @param string $source
|
||||
* @param string $link
|
||||
* @param string $method
|
||||
* @return boolean
|
||||
*/
|
||||
public static function link(string $source, string $link, string $method = 'link'): bool
|
||||
{
|
||||
Dir::make(dirname($link), true);
|
||||
|
||||
if (is_file($link) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_file($source) === false) {
|
||||
throw new Exception(sprintf('The file "%s" does not exist and cannot be linked', $source));
|
||||
}
|
||||
|
||||
return $method($source, $link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file and returns the result
|
||||
*
|
||||
* @param string $file
|
||||
* @return mixed
|
||||
*/
|
||||
public static function load(string $file, $fallback = null)
|
||||
{
|
||||
if (file_exists($file) === false) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$result = include $file;
|
||||
|
||||
if ($fallback !== null && gettype($result) !== gettype($fallback)) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type of a file
|
||||
*
|
||||
* @param string $file
|
||||
* @return string|false
|
||||
*/
|
||||
public static function mime(string $file)
|
||||
{
|
||||
return Mime::type($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a mime type to a file extension
|
||||
*
|
||||
* @param string $mime
|
||||
* @return string|false
|
||||
*/
|
||||
public static function mimeToExtension(string $mime = null)
|
||||
{
|
||||
return Mime::toExtension($mime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type for a given mime
|
||||
*
|
||||
* @param string $mime
|
||||
* @return string|false
|
||||
*/
|
||||
public static function mimeToType(string $mime)
|
||||
{
|
||||
return static::extensionToType(Mime::toExtension($mime));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's last modification time.
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $format
|
||||
* @param string $handler date or strftime
|
||||
* @return mixed
|
||||
*/
|
||||
public static function modified(string $file, string $format = null, string $handler = 'date')
|
||||
{
|
||||
if (file_exists($file) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stat = stat($file);
|
||||
$mtime = $stat['mtime'] ?? 0;
|
||||
$ctime = $stat['ctime'] ?? 0;
|
||||
$modified = max([$mtime, $ctime]);
|
||||
|
||||
if (is_null($format) === true) {
|
||||
return $modified;
|
||||
}
|
||||
|
||||
return $handler($format, $modified);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a file to a new location
|
||||
*
|
||||
* @param string $oldRoot The current path for the file
|
||||
* @param string $newRoot The path to the new location
|
||||
* @param boolean $force Force move if the target file exists
|
||||
* @return boolean
|
||||
*/
|
||||
public static function move(string $oldRoot, string $newRoot, bool $force = false): bool
|
||||
{
|
||||
// check if the file exists
|
||||
if (file_exists($oldRoot) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file_exists($newRoot) === true) {
|
||||
if ($force === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// delete the existing file
|
||||
unlink($newRoot);
|
||||
}
|
||||
|
||||
// actually move the file if it exists
|
||||
if (rename($oldRoot, $newRoot) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the name from a file path or filename without extension
|
||||
*
|
||||
* @param string $name The path or filename
|
||||
* @return string
|
||||
*/
|
||||
public static function name(string $name): string
|
||||
{
|
||||
return pathinfo($name, PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an integer size into a human readable format
|
||||
*
|
||||
* @param mixed $size The file size or a file path
|
||||
* @return string|int
|
||||
*/
|
||||
public static function niceSize($size): string
|
||||
{
|
||||
// file mode
|
||||
if (is_string($size) === true && file_exists($size) === true) {
|
||||
$size = static::size($size);
|
||||
}
|
||||
|
||||
// make sure it's an int
|
||||
$size = (int)$size;
|
||||
|
||||
// avoid errors for invalid sizes
|
||||
if ($size <= 0) {
|
||||
return '0 kB';
|
||||
}
|
||||
|
||||
// the math magic
|
||||
return round($size / pow(1024, ($i = floor(log($size, 1024)))), 2) . ' ' . static::$units[$i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the content of a file
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
* @return string|false
|
||||
*/
|
||||
public static function read(string $file)
|
||||
{
|
||||
return @file_get_contents($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the name of the file without
|
||||
* touching the extension
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $newName
|
||||
* @param bool $overwrite Force overwrite existing files
|
||||
* @return string|false
|
||||
*/
|
||||
public static function rename(string $file, string $newName, bool $overwrite = false)
|
||||
{
|
||||
// create the new name
|
||||
$name = static::safeName(basename($newName));
|
||||
|
||||
// overwrite the root
|
||||
$newRoot = rtrim(dirname($file) . '/' . $name . '.' . F::extension($file), '.');
|
||||
|
||||
// nothing has changed
|
||||
if ($newRoot === $file) {
|
||||
return $newRoot;
|
||||
}
|
||||
|
||||
if (F::move($file, $newRoot) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $newRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file if the file can be found.
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $in
|
||||
* @return string|null
|
||||
*/
|
||||
public static function realpath(string $file, string $in = null)
|
||||
{
|
||||
$realpath = realpath($file);
|
||||
|
||||
if ($realpath === false || is_file($realpath) === false) {
|
||||
throw new Exception(sprintf('The file does not exist at the given path: "%s"', $file));
|
||||
}
|
||||
|
||||
if ($in !== null) {
|
||||
$parent = realpath($in);
|
||||
|
||||
if ($parent === false || is_dir($parent) === false) {
|
||||
throw new Exception(sprintf('The parent directory does not exist: "%s"', $parent));
|
||||
}
|
||||
|
||||
if (substr($realpath, 0, strlen($parent)) !== $parent) {
|
||||
throw new Exception('The file is not within the parent directory');
|
||||
}
|
||||
}
|
||||
|
||||
return $realpath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* $remove = F::remove('test.txt');
|
||||
* if($remove) echo 'The file has been removed';
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
* @return boolean
|
||||
*/
|
||||
public static function remove(string $file): bool
|
||||
{
|
||||
if (strpos($file, '*') !== false) {
|
||||
foreach (glob($file) as $f) {
|
||||
static::remove($f);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$file = realpath($file);
|
||||
|
||||
if (file_exists($file) === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return unlink($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a filename to strip unwanted special characters
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* $safe = f::safeName('über genious.txt');
|
||||
* // safe will be ueber-genious.txt
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string The file name
|
||||
* @return string
|
||||
*/
|
||||
public static function safeName(string $string): string
|
||||
{
|
||||
$name = static::name($string);
|
||||
$extension = static::extension($string);
|
||||
$safeName = Str::slug($name, '-', 'a-z0-9@._-');
|
||||
$safeExtension = empty($extension) === false ? '.' . Str::slug($extension) : '';
|
||||
|
||||
return $safeName . $safeExtension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find similar or the same file by
|
||||
* building a glob based on the path
|
||||
*
|
||||
* @param string $path
|
||||
* @return array
|
||||
*/
|
||||
public static function similar(string $path, string $pattern = '*'): array
|
||||
{
|
||||
$dir = dirname($path);
|
||||
$name = static::name($path);
|
||||
$extension = static::extension($path);
|
||||
$glob = $dir . '/' . $name . $pattern . '.' . $extension;
|
||||
return glob($glob);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of a file.
|
||||
*
|
||||
* @param mixed $file The path
|
||||
* @return int
|
||||
*/
|
||||
public static function size(string $file): int
|
||||
{
|
||||
try {
|
||||
return filesize($file);
|
||||
} catch (Throwable $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize the file
|
||||
*
|
||||
* @param string $file Either the file path or extension
|
||||
* @return string|null
|
||||
*/
|
||||
public static function type(string $file)
|
||||
{
|
||||
$length = strlen($file);
|
||||
|
||||
if ($length >= 2 && $length <= 4) {
|
||||
// use the file name as extension
|
||||
$extension = $file;
|
||||
} else {
|
||||
// get the extension from the filename
|
||||
$extension = pathinfo($file, PATHINFO_EXTENSION);
|
||||
}
|
||||
|
||||
if (empty($extension) === true) {
|
||||
// detect the mime type first to get the most reliable extension
|
||||
$mime = static::mime($file);
|
||||
$extension = static::mimeToExtension($mime);
|
||||
}
|
||||
|
||||
// sanitize extension
|
||||
$extension = strtolower($extension);
|
||||
|
||||
foreach (static::$types as $type => $extensions) {
|
||||
if (in_array($extension, $extensions) === true) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzips a zip file
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $to
|
||||
* @return boolean
|
||||
*/
|
||||
public static function unzip(string $file, string $to): bool
|
||||
{
|
||||
if (class_exists('ZipArchive') === false) {
|
||||
throw new Exception('The ZipArchive class is not available');
|
||||
}
|
||||
|
||||
$zip = new ZipArchive;
|
||||
|
||||
if ($zip->open($file) === true) {
|
||||
$zip->extractTo($to);
|
||||
$zip->close();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file as data uri
|
||||
*
|
||||
* @param string $file The path for the file
|
||||
* @return string|false
|
||||
*/
|
||||
public static function uri(string $file)
|
||||
{
|
||||
if ($mime = static::mime($file)) {
|
||||
return 'data:' . $mime . ';base64,' . static::base64($file);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new file
|
||||
*
|
||||
* @param string $file The path for the new file
|
||||
* @param mixed $content Either a string, an object or an array. Arrays and objects will be serialized.
|
||||
* @param boolean $append true: append the content to an exisiting file if available. false: overwrite.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function write(string $file, $content, bool $append = false): bool
|
||||
{
|
||||
if (is_array($content) === true || is_object($content) === true) {
|
||||
$content = serialize($content);
|
||||
}
|
||||
|
||||
$mode = $append === true ? FILE_APPEND | LOCK_EX : LOCK_EX;
|
||||
|
||||
// if the parent directory does not exist, create it
|
||||
if (is_dir(dirname($file)) === false) {
|
||||
if (Dir::make(dirname($file)) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (static::isWritable($file) === false) {
|
||||
throw new Exception('The file "' . $file . '" is not writable');
|
||||
}
|
||||
|
||||
return file_put_contents($file, $content, $mode) !== false;
|
||||
}
|
||||
}
|
31
kirby/src/Toolkit/Facade.php
Executable file
31
kirby/src/Toolkit/Facade.php
Executable file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
/**
|
||||
* Laravel-style static facades
|
||||
* for class instances
|
||||
*/
|
||||
abstract class Facade
|
||||
{
|
||||
|
||||
/**
|
||||
* Returns the instance that should be
|
||||
* available statically
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected static function instance();
|
||||
|
||||
/**
|
||||
* Proxy for all public instance calls
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return mixed
|
||||
*/
|
||||
public static function __callStatic(string $method, array $args = null)
|
||||
{
|
||||
return static::instance()->$method(...$args);
|
||||
}
|
||||
}
|
336
kirby/src/Toolkit/File.php
Executable file
336
kirby/src/Toolkit/File.php
Executable file
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Flexible File object with a set of helpful
|
||||
* methods to inspect and work with files.
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license http://getkirby.com/license
|
||||
*/
|
||||
class File
|
||||
{
|
||||
|
||||
/**
|
||||
* Absolute file path
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* Constructs a new File object by absolute path
|
||||
*
|
||||
* @param string $root Absolute file path
|
||||
*/
|
||||
public function __construct(string $root = null)
|
||||
{
|
||||
$this->root = $root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved var_dump() output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debuginfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file content as base64 encoded string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function base64(): string
|
||||
{
|
||||
return base64_encode($this->read());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file to a new location.
|
||||
*
|
||||
* @param string $target
|
||||
* @param boolean $force
|
||||
* @return self
|
||||
*/
|
||||
public function copy(string $target, bool $force = false): self
|
||||
{
|
||||
if (F::copy($this->root, $target, $force) !== true) {
|
||||
throw new Exception('The file "' . $this->root . '" could not be copied');
|
||||
}
|
||||
|
||||
return new static($target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file as data uri
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dataUri(): string
|
||||
{
|
||||
return 'data:' . $this->mime() . ';base64,' . $this->base64();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(): bool
|
||||
{
|
||||
if (F::remove($this->root) !== true) {
|
||||
throw new Exception('The file "' . $this->root . '" could not be deleted');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file actually exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->root) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current lowercase extension (without .)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function extension(): string
|
||||
{
|
||||
return F::extension($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filename
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function filename(): string
|
||||
{
|
||||
return basename($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a md5 hash of the root
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function hash(): string
|
||||
{
|
||||
return md5($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file is of a certain type
|
||||
*
|
||||
* @param string $value An extension or mime type
|
||||
* @return bool
|
||||
*/
|
||||
public function is(string $value): bool
|
||||
{
|
||||
return F::is($this->root, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is readable
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isReadable(): bool
|
||||
{
|
||||
return is_readable($this->root) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the file is writable
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isWritable(): bool
|
||||
{
|
||||
return F::isWritable($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the mime type of the file
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function mime()
|
||||
{
|
||||
return Mime::type($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's last modification time.
|
||||
*
|
||||
* @param string $format
|
||||
* @param string $handler date or strftime
|
||||
* @return mixed
|
||||
*/
|
||||
public function modified(string $format = null, string $handler = 'date')
|
||||
{
|
||||
return F::modified($this->root, $format, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the file to a new location
|
||||
*
|
||||
* @param string $newRoot
|
||||
* @param bool $overwrite Force overwriting any existing files
|
||||
* @return self
|
||||
*/
|
||||
public function move(string $newRoot, bool $overwrite = false): self
|
||||
{
|
||||
if (F::move($this->root, $newRoot, $overwrite) !== true) {
|
||||
throw new Exception('The file: "' . $this->root . '" could not be moved to: "' . $newRoot . '"');
|
||||
}
|
||||
|
||||
return new static($newRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the name of the file
|
||||
* without the extension
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return pathinfo($this->root, PATHINFO_FILENAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file size in a
|
||||
* human-readable format
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function niceSize(): string
|
||||
{
|
||||
return F::niceSize($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the file content and returns it.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
return F::read($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the absolute path to the file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function realpath(): string
|
||||
{
|
||||
return realpath($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the name of the file without
|
||||
* touching the extension
|
||||
*
|
||||
* @param string $newName
|
||||
* @param bool $overwrite Force overwrite existing files
|
||||
* @return self
|
||||
*/
|
||||
public function rename(string $newName, bool $overwrite = false): self
|
||||
{
|
||||
$newRoot = F::rename($this->root, $newName, $overwrite);
|
||||
|
||||
if ($newRoot === false) {
|
||||
throw new Exception('The file: "' . $this->root . '" could not be renamed to: "' . $newName . '"');
|
||||
}
|
||||
|
||||
return new static($newRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the given file path
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function root(): ?string
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw size of the file
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function size(): int
|
||||
{
|
||||
return F::size($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the media object to a
|
||||
* plain PHP array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'root' => $this->root(),
|
||||
'hash' => $this->hash(),
|
||||
'filename' => $this->filename(),
|
||||
'name' => $this->name(),
|
||||
'safeName' => F::safeName($this->name()),
|
||||
'extension' => $this->extension(),
|
||||
'size' => $this->size(),
|
||||
'niceSize' => $this->niceSize(),
|
||||
'modified' => $this->modified('c'),
|
||||
'mime' => $this->mime(),
|
||||
'type' => $this->type(),
|
||||
'isWritable' => $this->isWritable(),
|
||||
'isReadable' => $this->isReadable(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file type.
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function type()
|
||||
{
|
||||
return F::type($this->root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes content to the file
|
||||
*
|
||||
* @param string $content
|
||||
* @return bool
|
||||
*/
|
||||
public function write($content): bool
|
||||
{
|
||||
if (F::write($this->root, $content) !== true) {
|
||||
throw new Exception('The file "' . $this->root . '" could not be written');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
508
kirby/src/Toolkit/Html.php
Executable file
508
kirby/src/Toolkit/Html.php
Executable file
@@ -0,0 +1,508 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Http\Url;
|
||||
|
||||
/**
|
||||
* Html builder for the most common elements
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
class Html
|
||||
{
|
||||
|
||||
/**
|
||||
* An internal store for a html entities translation table
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $entities;
|
||||
|
||||
/**
|
||||
* Can be used to switch to trailing slashes if required
|
||||
*
|
||||
* ```php
|
||||
* html::$void = ' />'
|
||||
* ```
|
||||
*
|
||||
* @var string $void
|
||||
*/
|
||||
public static $void = '>';
|
||||
|
||||
/**
|
||||
* Generic HTML tag generator
|
||||
*
|
||||
* @param string $tag
|
||||
* @param array $arguments
|
||||
* @return string
|
||||
*/
|
||||
public static function __callStatic(string $tag, array $arguments = []): string
|
||||
{
|
||||
if (static::isVoid($tag) === true) {
|
||||
return Html::tag($tag, null, ...$arguments);
|
||||
}
|
||||
|
||||
return Html::tag($tag, ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an a tag
|
||||
*
|
||||
* @param string $href The url for the a tag
|
||||
* @param mixed $text The optional text. If null, the url will be used as text
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string the generated html
|
||||
*/
|
||||
public static function a(string $href = null, $text = null, array $attr = []): string
|
||||
{
|
||||
$attr = array_merge(['href' => $href], $attr);
|
||||
|
||||
if (empty($text) === true) {
|
||||
$text = $href;
|
||||
}
|
||||
|
||||
if (is_string($text) === true && Str::isUrl($text) === true) {
|
||||
$text = Url::short($text);
|
||||
}
|
||||
|
||||
// add rel=noopener to target blank links to improve security
|
||||
$attr['rel'] = static::rel($attr['rel'] ?? null, $attr['target'] ?? null);
|
||||
|
||||
return static::tag('a', $text, $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a single attribute or a list of attributes
|
||||
*
|
||||
* @param string $name mixed string: a single attribute with that name will be generated. array: a list of attributes will be generated. Don't pass a second argument in that case.
|
||||
* @param string $value if used for a single attribute, pass the content for the attribute here
|
||||
* @return string the generated html
|
||||
*/
|
||||
public static function attr($name, $value = null): string
|
||||
{
|
||||
if (is_array($name) === true) {
|
||||
$attributes = [];
|
||||
|
||||
ksort($name);
|
||||
|
||||
foreach ($name as $key => $val) {
|
||||
$a = static::attr($key, $val);
|
||||
|
||||
if ($a) {
|
||||
$attributes[] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $attributes);
|
||||
}
|
||||
|
||||
if ($value === null || $value === '' || $value === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($value === ' ') {
|
||||
return strtolower($name) . '=""';
|
||||
}
|
||||
|
||||
if (is_bool($value) === true) {
|
||||
return $value === true ? strtolower($name) : '';
|
||||
}
|
||||
|
||||
if (is_array($value) === true) {
|
||||
if (isset($value['value']) && isset($value['escape'])) {
|
||||
$value = $value['escape'] === true ? htmlspecialchars($value['value'], ENT_QUOTES, 'UTF-8') : $value['value'];
|
||||
} else {
|
||||
$value = implode(' ', $value);
|
||||
}
|
||||
} else {
|
||||
$value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
return strtolower($name) . '="' . $value . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts lines in a string into html breaks
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function breaks(string $string = null): string
|
||||
{
|
||||
return nl2br($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all html tags and encoded chars from a string
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* echo html::decode('some uber <em>crazy</em> stuff');
|
||||
* // output: some uber crazy stuff
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string The html string
|
||||
*/
|
||||
public static function decode(string $string = null): string
|
||||
{
|
||||
$string = strip_tags($string);
|
||||
return html_entity_decode($string, ENT_COMPAT, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an "a mailto" tag
|
||||
*
|
||||
* @param string $email The url for the a tag
|
||||
* @param mixed $text The optional text. If null, the url will be used as text
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string the generated html
|
||||
*/
|
||||
public static function email(string $email, string $text = null, array $attr = []): string
|
||||
{
|
||||
if (empty($email) === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (empty($text) === true) {
|
||||
// show only the eMail address without additional parameters (if the 'text' argument is empty)
|
||||
$text = [Str::encode(Str::split($email, '?')[0])];
|
||||
}
|
||||
|
||||
$email = Str::encode($email);
|
||||
$attr = array_merge([
|
||||
'href' => [
|
||||
'value' => 'mailto:' . $email,
|
||||
'escape' => false
|
||||
]
|
||||
], $attr);
|
||||
|
||||
// add rel=noopener to target blank links to improve security
|
||||
$attr['rel'] = static::rel($attr['rel'] ?? null, $attr['target'] ?? null);
|
||||
|
||||
return static::tag('a', $text, $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to a html-safe string
|
||||
*
|
||||
* @param string $string
|
||||
* @param bool $keepTags
|
||||
* @return string The html string
|
||||
*/
|
||||
public static function encode(string $string = null, bool $keepTags = false): string
|
||||
{
|
||||
if ($keepTags === true) {
|
||||
$list = static::entities();
|
||||
unset($list['"'], $list['<'], $list['>'], $list['&']);
|
||||
|
||||
$search = array_keys($list);
|
||||
$values = array_values($list);
|
||||
|
||||
return str_replace($search, $values, $string);
|
||||
}
|
||||
|
||||
return htmlentities($string, ENT_COMPAT, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the entities translation table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function entities(): array
|
||||
{
|
||||
return static::$entities = static::$entities ?? get_html_translation_table(HTML_ENTITIES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a figure tag with optional caption
|
||||
*
|
||||
* @param string|array $content
|
||||
* @param string|array $caption
|
||||
* @param array $attr
|
||||
* @return string
|
||||
*/
|
||||
public static function figure($content, $caption = null, array $attr = []): string
|
||||
{
|
||||
if ($caption) {
|
||||
$figcaption = static::tag('figcaption', $caption);
|
||||
|
||||
if (is_string($content) === true) {
|
||||
$content = [static::encode($content, false)];
|
||||
}
|
||||
|
||||
$content[] = $figcaption;
|
||||
}
|
||||
|
||||
return static::tag('figure', $content, $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a gist
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $file
|
||||
* @param array $attr
|
||||
* @return string
|
||||
*/
|
||||
public static function gist(string $url, string $file = null, array $attr = []): string
|
||||
{
|
||||
if ($file === null) {
|
||||
$src = $url . '.js';
|
||||
} else {
|
||||
$src = $url . '.js?file=' . $file;
|
||||
}
|
||||
|
||||
return static::tag('script', null, array_merge($attr, [
|
||||
'src' => $src
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an iframe
|
||||
*
|
||||
* @param string $src
|
||||
* @param array $attr
|
||||
* @return string
|
||||
*/
|
||||
public static function iframe(string $src, array $attr = []): string
|
||||
{
|
||||
return static::tag('iframe', null, array_merge(['src' => $src], $attr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an img tag
|
||||
*
|
||||
* @param string $src The url of the image
|
||||
* @param array $attr Additional attributes for the image tag
|
||||
* @return string the generated html
|
||||
*/
|
||||
public static function img(string $src, array $attr = []): string
|
||||
{
|
||||
$attr = array_merge([
|
||||
'src' => $src,
|
||||
'alt' => ' '
|
||||
], $attr);
|
||||
|
||||
return static::tag('img', null, $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tag is self-closing
|
||||
*
|
||||
* @param string $tag
|
||||
* @return bool
|
||||
*/
|
||||
public static function isVoid(string $tag): bool
|
||||
{
|
||||
$void = [
|
||||
'area',
|
||||
'base',
|
||||
'br',
|
||||
'col',
|
||||
'command',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
];
|
||||
|
||||
return in_array(strtolower($tag), $void);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add noopeener noreferrer to rels when target is _blank
|
||||
*
|
||||
* @param string $rel
|
||||
* @param string $target
|
||||
* @return string|null
|
||||
*/
|
||||
public static function rel(string $rel = null, string $target = null)
|
||||
{
|
||||
if ($target === '_blank') {
|
||||
return trim($rel . ' noopener noreferrer');
|
||||
}
|
||||
|
||||
return $rel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an Html tag with optional content and attributes
|
||||
*
|
||||
* @param string $name The name of the tag, i.e. "a"
|
||||
* @param mixed $content The content if availble. Pass null to generate a self-closing tag, Pass an empty string to generate empty content
|
||||
* @param array $attr An associative array with additional attributes for the tag
|
||||
* @return string The generated Html
|
||||
*/
|
||||
public static function tag(string $name, $content = null, array $attr = []): string
|
||||
{
|
||||
$html = '<' . $name;
|
||||
$attr = static::attr($attr);
|
||||
|
||||
if (empty($attr) === false) {
|
||||
$html .= ' ' . $attr;
|
||||
}
|
||||
|
||||
if (static::isVoid($name) === true) {
|
||||
$html .= static::$void;
|
||||
} else {
|
||||
if (is_array($content) === true) {
|
||||
$content = implode($content);
|
||||
} else {
|
||||
$content = static::encode($content, false);
|
||||
}
|
||||
|
||||
$html .= '>' . $content . '</' . $name . '>';
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates an a tag for a phone number
|
||||
*
|
||||
* @param string $tel The phone number
|
||||
* @param mixed $text The optional text. If null, the number will be used as text
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string the generated html
|
||||
*/
|
||||
public static function tel($tel = null, $text = null, array $attr = []): string
|
||||
{
|
||||
$number = preg_replace('![^0-9\+]+!', '', $tel);
|
||||
|
||||
if (empty($text) === true) {
|
||||
$text = $tel;
|
||||
}
|
||||
|
||||
return static::a('tel:' . $number, $text, $attr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a video embed via iframe for Youtube or Vimeo
|
||||
* videos. The embed Urls are automatically detected from
|
||||
* the given Url.
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param array $attr
|
||||
* @return string
|
||||
*/
|
||||
public static function video(string $url, ?array $options = [], array $attr = []): string
|
||||
{
|
||||
// YouTube video
|
||||
if (preg_match('!youtu!i', $url) === 1) {
|
||||
return static::youtube($url, $options['youtube'] ?? [], $attr);
|
||||
}
|
||||
|
||||
// Vimeo video
|
||||
if (preg_match('!vimeo!i', $url) === 1) {
|
||||
return static::vimeo($url, $options['vimeo'] ?? [], $attr);
|
||||
}
|
||||
|
||||
throw new Exception('Unexpected video type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a Vimeo video by URL in an iframe
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param array $attr
|
||||
* @return string
|
||||
*/
|
||||
public static function vimeo(string $url, ?array $options = [], array $attr = []): string
|
||||
{
|
||||
if (preg_match('!vimeo.com\/([0-9]+)!i', $url, $array) === 1) {
|
||||
$id = $array[1];
|
||||
} elseif (preg_match('!player.vimeo.com\/video\/([0-9]+)!i', $url, $array) === 1) {
|
||||
$id = $array[1];
|
||||
} else {
|
||||
throw new Exception('Invalid Vimeo source');
|
||||
}
|
||||
|
||||
// build the options query
|
||||
if (!empty($options)) {
|
||||
$query = '?' . http_build_query($options);
|
||||
} else {
|
||||
$query = '';
|
||||
}
|
||||
|
||||
$url = 'https://player.vimeo.com/video/' . $id . $query;
|
||||
|
||||
return static::iframe($url, array_merge(['allowfullscreen' => true], $attr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a Youtube video by URL in an iframe
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @param array $attr
|
||||
* @return string
|
||||
*/
|
||||
public static function youtube(string $url, ?array $options = [], array $attr = []): string
|
||||
{
|
||||
// youtube embed domain
|
||||
$domain = 'youtube.com';
|
||||
$id = null;
|
||||
|
||||
$schemes = [
|
||||
// http://www.youtube.com/embed/d9NF2edxy-M
|
||||
['pattern' => 'youtube.com\/embed\/([a-zA-Z0-9_-]+)'],
|
||||
// https://www.youtube-nocookie.com/embed/d9NF2edxy-M
|
||||
[
|
||||
'pattern' => 'youtube-nocookie.com\/embed\/([a-zA-Z0-9_-]+)',
|
||||
'domain' => 'www.youtube-nocookie.com'
|
||||
],
|
||||
// https://www.youtube-nocookie.com/watch?v=d9NF2edxy-M
|
||||
[
|
||||
'pattern' => 'youtube-nocookie.com\/watch\?v=([a-zA-Z0-9_-]+)',
|
||||
'domain' => 'www.youtube-nocookie.com'
|
||||
],
|
||||
// http://www.youtube.com/watch?v=d9NF2edxy-M
|
||||
['pattern' => 'v=([a-zA-Z0-9_-]+)'],
|
||||
// http://youtu.be/d9NF2edxy-M
|
||||
['pattern' => 'youtu.be\/([a-zA-Z0-9_-]+)']
|
||||
];
|
||||
|
||||
foreach ($schemes as $schema) {
|
||||
if (preg_match('!' . $schema['pattern'] . '!i', $url, $array) === 1) {
|
||||
$domain = $schema['domain'] ?? $domain;
|
||||
$id = $array[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// no match
|
||||
if ($id === null) {
|
||||
throw new Exception('Invalid Youtube source');
|
||||
}
|
||||
|
||||
// build the options query
|
||||
if (!empty($options)) {
|
||||
$query = '?' . http_build_query($options);
|
||||
} else {
|
||||
$query = '';
|
||||
}
|
||||
|
||||
$url = 'https://' . $domain . '/embed/' . $id . $query;
|
||||
|
||||
return static::iframe($url, array_merge(['allowfullscreen' => true], $attr));
|
||||
}
|
||||
}
|
224
kirby/src/Toolkit/I18n.php
Executable file
224
kirby/src/Toolkit/I18n.php
Executable file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Localization class, roughly inspired by VueI18n
|
||||
*/
|
||||
class I18n
|
||||
{
|
||||
|
||||
/**
|
||||
* Custom loader function
|
||||
*
|
||||
* @var Closure
|
||||
*/
|
||||
public static $load = null;
|
||||
|
||||
/**
|
||||
* Current locale
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $locale = 'en';
|
||||
|
||||
/**
|
||||
* All registered translations
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $translations = [];
|
||||
|
||||
/**
|
||||
* The fallback locale
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $fallback = 'en';
|
||||
|
||||
/**
|
||||
* Returns the fallback code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function fallback(): string
|
||||
{
|
||||
if (is_string(static::$fallback) === true) {
|
||||
return static::$fallback;
|
||||
}
|
||||
|
||||
if (is_callable(static::$fallback) === true) {
|
||||
return static::$fallback = (static::$fallback)();
|
||||
}
|
||||
|
||||
return static::$fallback = 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns singular or plural
|
||||
* depending on the given number
|
||||
*
|
||||
* @param int $count
|
||||
* @param boolean $none If true, 'none' will be returned if the count is 0
|
||||
* @return string
|
||||
*/
|
||||
public static function form(int $count, bool $none = false): string
|
||||
{
|
||||
if ($none === true && $count === 0) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
return $count === 1 ? 'singular' : 'plural';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function locale(): string
|
||||
{
|
||||
if (is_string(static::$locale) === true) {
|
||||
return static::$locale;
|
||||
}
|
||||
|
||||
if (is_callable(static::$locale) === true) {
|
||||
return static::$locale = (static::$locale)();
|
||||
}
|
||||
|
||||
return static::$locale = 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given message
|
||||
* according to the currently set locale
|
||||
*
|
||||
* @param string|array $key
|
||||
* @param string|array|null $fallback
|
||||
* @param string|null $locale
|
||||
* @return string|array|null
|
||||
*/
|
||||
public static function translate($key, $fallback = null, string $locale = null)
|
||||
{
|
||||
$locale = $locale ?? static::locale();
|
||||
|
||||
if (is_array($key) === true) {
|
||||
if (isset($key[$locale])) {
|
||||
return $key[$locale];
|
||||
}
|
||||
if (is_array($fallback)) {
|
||||
return $fallback[$locale] ?? null;
|
||||
}
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
if ($translation = static::translation($locale)[$key] ?? null) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
if ($fallback !== null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
if ($locale !== static::fallback()) {
|
||||
return static::translation(static::fallback())[$key] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate by key and then replace
|
||||
* placeholders in the text
|
||||
*
|
||||
* @param string $key
|
||||
* @param string $fallback
|
||||
* @param array $replace
|
||||
* @param string $locale
|
||||
* @return string
|
||||
*/
|
||||
public static function template(string $key, $fallback = null, array $replace = null, string $locale = null)
|
||||
{
|
||||
if (is_array($fallback) === true) {
|
||||
$replace = $fallback;
|
||||
$fallback = null;
|
||||
$locale = null;
|
||||
}
|
||||
|
||||
$template = static::translate($key, $fallback, $locale);
|
||||
return Str::template($template, $replace, '-', '{', '}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current or any other translation
|
||||
* by locale. If the translation does not exist
|
||||
* yet, the loader will try to load it, if defined.
|
||||
*
|
||||
* @param string|null $locale
|
||||
* @return array
|
||||
*/
|
||||
public static function translation(string $locale = null): array
|
||||
{
|
||||
$locale = $locale ?? static::locale();
|
||||
|
||||
if (isset(static::$translations[$locale]) === true) {
|
||||
return static::$translations[$locale];
|
||||
}
|
||||
|
||||
if (is_a(static::$load, 'Closure') === true) {
|
||||
return static::$translations[$locale] = (static::$load)($locale);
|
||||
}
|
||||
|
||||
return static::$translations[$locale] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all loaded or defined translations
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function translations(): array
|
||||
{
|
||||
return static::$translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate amounts
|
||||
*
|
||||
* @param string $key
|
||||
* @param integer $count
|
||||
* @return mixed
|
||||
*/
|
||||
public static function translateCount(string $key, int $count, string $locale = null)
|
||||
{
|
||||
$translation = static::translate($key, null, $locale);
|
||||
|
||||
if ($translation === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_string($translation) === true) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
if (count($translation) !== 3) {
|
||||
throw new Exception('Please provide 3 translations');
|
||||
}
|
||||
|
||||
switch ($count) {
|
||||
case 0:
|
||||
$message = $translation[0];
|
||||
break;
|
||||
case 1:
|
||||
$message = $translation[1];
|
||||
break;
|
||||
default:
|
||||
$message = $translation[2];
|
||||
}
|
||||
|
||||
return str_replace('{{ count }}', $count, $message);
|
||||
}
|
||||
}
|
163
kirby/src/Toolkit/Iterator.php
Executable file
163
kirby/src/Toolkit/Iterator.php
Executable file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
/**
|
||||
* Extended version of PHP's iterator
|
||||
* class that builds the foundation of our
|
||||
* Collection class.
|
||||
*/
|
||||
class Iterator implements \Iterator
|
||||
{
|
||||
|
||||
/**
|
||||
* The data array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current key from the array
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return key($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all keys in the Iterator
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
return array_keys($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current element of the array
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return current($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the cursor to the previous element in the array
|
||||
* and returns it
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function prev()
|
||||
{
|
||||
return prev($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the cursor to the next element in the array
|
||||
* and returns it
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
return next($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the cusor to the first element of the array
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
reset($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current element is valid
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
return $this->current() !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts all elements in the array
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the index number for the given element
|
||||
*
|
||||
* @param mixed $needle the element to search for
|
||||
* @return string|false the name of the key or false
|
||||
*/
|
||||
public function indexOf($needle)
|
||||
{
|
||||
return array_search($needle, array_values($this->data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the key for the given element
|
||||
*
|
||||
* @param mixed $needle the element to search for
|
||||
* @return string|false the name of the key or false
|
||||
*/
|
||||
public function keyOf($needle)
|
||||
{
|
||||
return array_search($needle, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an element is in the collection by key.
|
||||
*
|
||||
* @param mixed $key
|
||||
* @return boolean
|
||||
*/
|
||||
public function has($key): bool
|
||||
{
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current key is set
|
||||
*
|
||||
* @param mixed $key the key to check
|
||||
* @return boolean
|
||||
*/
|
||||
public function __isset($key): bool
|
||||
{
|
||||
return $this->has($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified var_dump output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debuginfo(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
285
kirby/src/Toolkit/Mime.php
Executable file
285
kirby/src/Toolkit/Mime.php
Executable file
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use SimpleXMLElement;
|
||||
|
||||
/**
|
||||
* Mime type detection/guessing
|
||||
*/
|
||||
class Mime
|
||||
{
|
||||
/**
|
||||
* Extension to mime type map
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $types = [
|
||||
'ai' => 'application/postscript',
|
||||
'aif' => 'audio/x-aiff',
|
||||
'aifc' => 'audio/x-aiff',
|
||||
'aiff' => 'audio/x-aiff',
|
||||
'avi' => 'video/x-msvideo',
|
||||
'bmp' => 'image/bmp',
|
||||
'css' => 'text/css',
|
||||
'csv' => ['text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'],
|
||||
'doc' => 'application/msword',
|
||||
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||
'dvi' => 'application/x-dvi',
|
||||
'eml' => 'message/rfc822',
|
||||
'eps' => 'application/postscript',
|
||||
'exe' => ['application/octet-stream', 'application/x-msdownload'],
|
||||
'gif' => 'image/gif',
|
||||
'gtar' => 'application/x-gtar',
|
||||
'gz' => 'application/x-gzip',
|
||||
'htm' => 'text/html',
|
||||
'html' => 'text/html',
|
||||
'ico' => 'image/x-icon',
|
||||
'ics' => 'text/calendar',
|
||||
'js' => 'application/x-javascript',
|
||||
'json' => ['application/json', 'text/json'],
|
||||
'jpg' => ['image/jpeg', 'image/pjpeg'],
|
||||
'jpeg' => ['image/jpeg', 'image/pjpeg'],
|
||||
'jpe' => ['image/jpeg', 'image/pjpeg'],
|
||||
'log' => ['text/plain', 'text/x-log'],
|
||||
'm4a' => 'audio/mp4',
|
||||
'm4v' => 'video/mp4',
|
||||
'mid' => 'audio/midi',
|
||||
'midi' => 'audio/midi',
|
||||
'mif' => 'application/vnd.mif',
|
||||
'mov' => 'video/quicktime',
|
||||
'movie' => 'video/x-sgi-movie',
|
||||
'mp2' => 'audio/mpeg',
|
||||
'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'],
|
||||
'mp4' => 'video/mp4',
|
||||
'mpe' => 'video/mpeg',
|
||||
'mpeg' => 'video/mpeg',
|
||||
'mpg' => 'video/mpeg',
|
||||
'mpga' => 'audio/mpeg',
|
||||
'odc' => 'application/vnd.oasis.opendocument.chart',
|
||||
'odp' => 'application/vnd.oasis.opendocument.presentation',
|
||||
'odt' => 'application/vnd.oasis.opendocument.text',
|
||||
'pdf' => ['application/pdf', 'application/x-download'],
|
||||
'php' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'php3' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'phps' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'phtml' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'],
|
||||
'png' => 'image/png',
|
||||
'ppt' => ['application/powerpoint', 'application/vnd.ms-powerpoint'],
|
||||
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||
'ps' => 'application/postscript',
|
||||
'psd' => 'application/x-photoshop',
|
||||
'qt' => 'video/quicktime',
|
||||
'rss' => 'application/rss+xml',
|
||||
'rtf' => 'text/rtf',
|
||||
'rtx' => 'text/richtext',
|
||||
'shtml' => 'text/html',
|
||||
'svg' => 'image/svg+xml',
|
||||
'swf' => 'application/x-shockwave-flash',
|
||||
'tar' => 'application/x-tar',
|
||||
'text' => 'text/plain',
|
||||
'txt' => 'text/plain',
|
||||
'tgz' => ['application/x-tar', 'application/x-gzip-compressed'],
|
||||
'tif' => 'image/tiff',
|
||||
'tiff' => 'image/tiff',
|
||||
'wav' => 'audio/x-wav',
|
||||
'wbxml' => 'application/wbxml',
|
||||
'webm' => 'video/webm',
|
||||
'webp' => 'image/webp',
|
||||
'word' => ['application/msword', 'application/octet-stream'],
|
||||
'xhtml' => 'application/xhtml+xml',
|
||||
'xht' => 'application/xhtml+xml',
|
||||
'xml' => 'text/xml',
|
||||
'xl' => 'application/excel',
|
||||
'xls' => ['application/excel', 'application/vnd.ms-excel', 'application/msexcel'],
|
||||
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
'xsl' => 'text/xml',
|
||||
'zip' => ['application/x-zip', 'application/zip', 'application/x-zip-compressed'],
|
||||
];
|
||||
|
||||
/**
|
||||
* Fixes an invalid mime type guess for the given file
|
||||
*
|
||||
* @param string $file
|
||||
* @param string $mime
|
||||
* @param string $extension
|
||||
* @return string|null
|
||||
*/
|
||||
public static function fix(string $file, string $mime = null, string $extension = null)
|
||||
{
|
||||
// fixing map
|
||||
$map = [
|
||||
'text/html' => [
|
||||
'svg' => [Mime::class, 'fromSvg'],
|
||||
],
|
||||
'text/plain' => [
|
||||
'css' => 'text/css',
|
||||
'svg' => [Mime::class, 'fromSvg'],
|
||||
],
|
||||
'text/x-asm' => [
|
||||
'css' => 'text/css'
|
||||
],
|
||||
'image/svg' => [
|
||||
'svg' => 'image/svg+xml'
|
||||
]
|
||||
];
|
||||
|
||||
if ($mode = ($map[$mime][$extension] ?? null)) {
|
||||
if (is_callable($mode) === true) {
|
||||
return $mode($file, $mime, $extension);
|
||||
}
|
||||
|
||||
if (is_string($mode) === true) {
|
||||
return $mode;
|
||||
}
|
||||
}
|
||||
|
||||
return $mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guesses a mime type by extension
|
||||
*
|
||||
* @param string $extension
|
||||
* @return string|null
|
||||
*/
|
||||
public static function fromExtension(string $extension)
|
||||
{
|
||||
$mime = static::$types[$extension] ?? null;
|
||||
return is_array($mime) === true ? array_shift($mime) : $mime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type of a file
|
||||
*
|
||||
* @param string $file
|
||||
* @return string|false
|
||||
*/
|
||||
public static function fromFileInfo(string $file)
|
||||
{
|
||||
if (function_exists('finfo_file') === true && file_exists($file) === true) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
$mime = finfo_file($finfo, $file);
|
||||
finfo_close($finfo);
|
||||
return $mime;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type of a file
|
||||
*
|
||||
* @param string $file
|
||||
* @return string|false
|
||||
*/
|
||||
public static function fromMimeContentType(string $file)
|
||||
{
|
||||
if (function_exists('mime_content_type') === true && file_exists($file) === true) {
|
||||
return mime_content_type($file);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to detect a valid SVG and returns the mime type accordingly
|
||||
*
|
||||
* @param string $file
|
||||
* @return string|false
|
||||
*/
|
||||
public static function fromSvg(string $file)
|
||||
{
|
||||
if (file_exists($file) === true) {
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$svg = new SimpleXMLElement(file_get_contents($file));
|
||||
|
||||
if ($svg !== false && $svg->getName() === 'svg') {
|
||||
return 'image/svg+xml';
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Undocumented function
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isAccepted(string $mime, string $pattern): bool
|
||||
{
|
||||
$accepted = Str::accepted($pattern);
|
||||
|
||||
foreach ($accepted as $m) {
|
||||
if (fnmatch($m['value'], $mime, FNM_PATHNAME) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the extension for a given mime type
|
||||
*
|
||||
* @param string|null $mime
|
||||
* @return string|false
|
||||
*/
|
||||
public static function toExtension(string $mime = null)
|
||||
{
|
||||
foreach (static::$types as $key => $value) {
|
||||
if (is_array($value) === true && in_array($mime, $value) === true) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
if ($value === $mime) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mime type of a file
|
||||
*
|
||||
* @param string $file
|
||||
* @return string|false
|
||||
*/
|
||||
public static function type(string $file, string $extension = null)
|
||||
{
|
||||
// use the standard finfo extension
|
||||
$mime = static::fromFileInfo($file);
|
||||
|
||||
// use the mime_content_type function
|
||||
if ($mime === false) {
|
||||
$mime = static::fromMimeContentType($file);
|
||||
}
|
||||
|
||||
// get the extension or extract it from the filename
|
||||
$extension = $extension ?? F::extension($file);
|
||||
|
||||
// try to guess the mime type at least
|
||||
if ($mime === false) {
|
||||
$mime = static::fromExtension($extension);
|
||||
}
|
||||
|
||||
// fix broken mime detection
|
||||
return static::fix($file, $mime, $extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all detectable mime types
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function types(): array
|
||||
{
|
||||
return static::$types;
|
||||
}
|
||||
}
|
100
kirby/src/Toolkit/Obj.php
Executable file
100
kirby/src/Toolkit/Obj.php
Executable file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Super simple stdClass extension with
|
||||
* magic getter methods for all properties
|
||||
*/
|
||||
class Obj extends stdClass
|
||||
{
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
foreach ($data as $key => $val) {
|
||||
$this->$key = $val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter
|
||||
*
|
||||
* @param string $property
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $property, array $arguments)
|
||||
{
|
||||
return $this->$property ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved var_dump() output
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debuginfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic property getter
|
||||
*
|
||||
* @param string $property
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $property)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Property Getter
|
||||
*
|
||||
* @param string $property
|
||||
* @param mixed $fallback
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $property, $fallback = null)
|
||||
{
|
||||
return $this->$property ?? $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach ((array)$this as $key => $value) {
|
||||
if (is_object($value) === true && method_exists($value, 'toArray')) {
|
||||
$result[$key] = $value->toArray();
|
||||
} else {
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object to a json string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toJson(...$arguments): string
|
||||
{
|
||||
return json_encode($this->toArray(), ...$arguments);
|
||||
}
|
||||
}
|
410
kirby/src/Toolkit/Pagination.php
Executable file
410
kirby/src/Toolkit/Pagination.php
Executable file
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Basic pagination handling
|
||||
*/
|
||||
class Pagination
|
||||
{
|
||||
|
||||
/**
|
||||
* The current page
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* Total number of items
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $total;
|
||||
|
||||
/**
|
||||
* The number of items per page
|
||||
*
|
||||
* @var integer
|
||||
*/
|
||||
protected $limit;
|
||||
|
||||
/**
|
||||
* Creates a new pagination object
|
||||
* with the given parameters
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->page($params['page'] ?? 1);
|
||||
$this->limit($params['limit'] ?? 20);
|
||||
$this->total($params['total'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pagination instance for the given
|
||||
* collection with a flexible argument api
|
||||
*
|
||||
* @param Collection $collection
|
||||
* @param ...mixed $arguments
|
||||
* @return self
|
||||
*/
|
||||
public static function for(Collection $collection, ...$arguments)
|
||||
{
|
||||
$a = $arguments[0] ?? null;
|
||||
$b = $arguments[1] ?? null;
|
||||
|
||||
$params = [];
|
||||
|
||||
if (is_array($a) === true) {
|
||||
|
||||
/**
|
||||
* First argument is an option array
|
||||
*
|
||||
* $collection->paginate([...])
|
||||
*/
|
||||
$params = $a;
|
||||
} elseif (is_int($a) === true && $b === null) {
|
||||
|
||||
/**
|
||||
* First argument is the limit
|
||||
*
|
||||
* $collection->paginate(10)
|
||||
*/
|
||||
$params['limit'] = $a;
|
||||
} elseif (is_int($a) === true && is_int($b) === true) {
|
||||
|
||||
/**
|
||||
* First argument is the limit,
|
||||
* second argument is the page
|
||||
*
|
||||
* $collection->paginate(10, 2)
|
||||
*/
|
||||
$params['limit'] = $a;
|
||||
$params['page'] = $b;
|
||||
} elseif (is_int($a) === true && is_array($b) === true) {
|
||||
|
||||
/**
|
||||
* First argument is the limit,
|
||||
* second argument are options
|
||||
*
|
||||
* $collection->paginate(10, [...])
|
||||
*/
|
||||
$params = $b;
|
||||
$params['limit'] = $a;
|
||||
}
|
||||
|
||||
// add the total count from the collection
|
||||
$params['total'] = $collection->count();
|
||||
|
||||
// remove null values to make later merges work properly
|
||||
$params = array_filter($params);
|
||||
|
||||
// create the pagination instance
|
||||
return new static($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter and setter for the current page
|
||||
*
|
||||
* @param int|null $page
|
||||
* @return int|Pagination
|
||||
*/
|
||||
public function page(int $page = null)
|
||||
{
|
||||
if ($page === null) {
|
||||
if ($this->page > $this->pages()) {
|
||||
$this->page = $this->lastPage();
|
||||
}
|
||||
|
||||
if ($this->page < 1) {
|
||||
$this->page = $this->firstPage();
|
||||
}
|
||||
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
$this->page = $page;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter and setter for the total number of items
|
||||
*
|
||||
* @param int|null $total
|
||||
* @return int|Pagination
|
||||
*/
|
||||
public function total(int $total = null)
|
||||
{
|
||||
if ($total === null) {
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
if ($total < 0) {
|
||||
throw new Exception('Invalid total number of items: ' . $total);
|
||||
}
|
||||
|
||||
$this->total = $total;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter and setter for the number of items per page
|
||||
*
|
||||
* @param int|null $limit
|
||||
* @return int|Pagination
|
||||
*/
|
||||
public function limit(int $limit = null)
|
||||
{
|
||||
if ($limit === null) {
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
if ($limit < 1) {
|
||||
throw new Exception('Invalid pagination limit: ' . $limit);
|
||||
}
|
||||
|
||||
$this->limit = $limit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the first item on the page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function start(): int
|
||||
{
|
||||
$index = $this->page() - 1;
|
||||
|
||||
if ($index < 0) {
|
||||
$index = 0;
|
||||
}
|
||||
|
||||
return $index * $this->limit() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the last item on the page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function end(): int
|
||||
{
|
||||
$value = ($this->start() - 1) + $this->limit();
|
||||
|
||||
if ($value <= $this->total()) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return $this->total();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of pages
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function pages(): int
|
||||
{
|
||||
if ($this->total() === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ceil($this->total() / $this->limit());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function firstPage(): int
|
||||
{
|
||||
return $this->total() === 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function lastPage(): int
|
||||
{
|
||||
return $this->pages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the offset (i.e. for db queries)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function offset(): int
|
||||
{
|
||||
return $this->start() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given page exists
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasPage(int $page): bool
|
||||
{
|
||||
if ($page <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($page > $this->pages()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there are any pages at all
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasPages(): bool
|
||||
{
|
||||
return $this->total() > $this->limit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a previous page
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasPrevPage(): bool
|
||||
{
|
||||
return $this->page() > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous page
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function prevPage()
|
||||
{
|
||||
return $this->hasPrevPage() ? $this->page() - 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a next page
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasNextPage(): bool
|
||||
{
|
||||
return $this->end() < $this->total();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next page
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function nextPage()
|
||||
{
|
||||
return $this->hasNextPage() ? $this->page() + 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current page is the first page
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isFirstPage(): bool
|
||||
{
|
||||
return $this->page() === $this->firstPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current page is the last page
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isLastPage(): bool
|
||||
{
|
||||
return $this->page() === $this->lastPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a range of page numbers for Google-like pagination
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function range(int $range = 5): array
|
||||
{
|
||||
$page = $this->page();
|
||||
$pages = $this->pages();
|
||||
$start = 1;
|
||||
$end = $pages;
|
||||
|
||||
if ($pages <= $range) {
|
||||
return range($start, $end);
|
||||
}
|
||||
|
||||
$start = $page - (int)floor($range/2);
|
||||
$end = $page + (int)floor($range/2);
|
||||
|
||||
if ($start <= 0) {
|
||||
$end += abs($start);
|
||||
$start = 1;
|
||||
}
|
||||
|
||||
if ($end > $pages) {
|
||||
$start -= $end - $pages;
|
||||
$end = $pages;
|
||||
}
|
||||
|
||||
return range($start, $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first page of the created range
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function rangeStart(int $range = 5): int
|
||||
{
|
||||
return $this->range($range)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last page of the created range
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function rangeEnd(int $range = 5): int
|
||||
{
|
||||
$range = $this->range($range);
|
||||
return array_pop($range);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all properties
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'page' => $this->page(),
|
||||
'firstPage' => $this->firstPage(),
|
||||
'lastPage' => $this->lastPage(),
|
||||
'pages' => $this->pages(),
|
||||
'offset' => $this->offset(),
|
||||
'limit' => $this->limit(),
|
||||
'total' => $this->total(),
|
||||
'start' => $this->start(),
|
||||
'end' => $this->end(),
|
||||
];
|
||||
}
|
||||
}
|
143
kirby/src/Toolkit/Properties.php
Executable file
143
kirby/src/Toolkit/Properties.php
Executable file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
use ReflectionClass;
|
||||
use ReflectionMethod;
|
||||
|
||||
trait Properties
|
||||
{
|
||||
protected $propertyData = [];
|
||||
|
||||
/**
|
||||
* Creates an instance with the same
|
||||
* initial properties.
|
||||
*
|
||||
* @param array $props
|
||||
* @return self
|
||||
*/
|
||||
public function clone(array $props = [])
|
||||
{
|
||||
return new static(array_replace_recursive($this->propertyData, $props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a clone and fetches all
|
||||
* lazy-loaded getters to get a full copy
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function hardcopy()
|
||||
{
|
||||
$clone = $this->clone();
|
||||
$clone->propertiesToArray();
|
||||
return $clone;
|
||||
}
|
||||
|
||||
protected function isRequiredProperty(string $name): bool
|
||||
{
|
||||
$method = new ReflectionMethod($this, 'set' . $name);
|
||||
return $method->getNumberOfRequiredParameters() > 0;
|
||||
}
|
||||
|
||||
protected function propertiesToArray()
|
||||
{
|
||||
$array = [];
|
||||
|
||||
foreach (get_object_vars($this) as $name => $default) {
|
||||
if ($name === 'propertyData') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method_exists($this, 'convert' . $name . 'ToArray') === true) {
|
||||
$array[$name] = $this->{'convert' . $name . 'ToArray'}();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (method_exists($this, $name) === true) {
|
||||
$method = new ReflectionMethod($this, $name);
|
||||
|
||||
if ($method->isPublic() === true) {
|
||||
$value = $this->$name();
|
||||
|
||||
if (is_object($value) === false) {
|
||||
$array[$name] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ksort($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
protected function setOptionalProperties(array $props, array $optional)
|
||||
{
|
||||
$this->propertyData = array_merge($this->propertyData, $props);
|
||||
|
||||
foreach ($optional as $propertyName) {
|
||||
if (isset($props[$propertyName]) === true) {
|
||||
$this->{'set' . $propertyName}($props[$propertyName]);
|
||||
} else {
|
||||
$this->{'set' . $propertyName}();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function setProperties($props, array $keys = null)
|
||||
{
|
||||
foreach (get_object_vars($this) as $name => $default) {
|
||||
if ($name === 'propertyData') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->setProperty($name, $props[$name] ?? $default);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function setProperty($name, $value, $required = null)
|
||||
{
|
||||
// use a setter if it exists
|
||||
if (method_exists($this, 'set' . $name) === false) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// fetch the default value from the property
|
||||
$value = $value ?? $this->$name ?? null;
|
||||
|
||||
// store all original properties, to be able to clone them later
|
||||
$this->propertyData[$name] = $value;
|
||||
|
||||
// handle empty values
|
||||
if ($value === null) {
|
||||
|
||||
// replace null with a default value, if a default handler exists
|
||||
if (method_exists($this, 'default' . $name) === true) {
|
||||
$value = $this->{'default' . $name}();
|
||||
}
|
||||
|
||||
// check for required properties
|
||||
if ($value === null && ($required ?? $this->isRequiredProperty($name)) === true) {
|
||||
throw new Exception(sprintf('The property "%s" is required', $name));
|
||||
}
|
||||
}
|
||||
|
||||
// call the setter with the final value
|
||||
return $this->{'set' . $name}($value);
|
||||
}
|
||||
|
||||
protected function setRequiredProperties(array $props, array $required)
|
||||
{
|
||||
foreach ($required as $propertyName) {
|
||||
if (isset($props[$propertyName]) !== true) {
|
||||
throw new Exception(sprintf('The property "%s" is required', $propertyName));
|
||||
}
|
||||
|
||||
$this->{'set' . $propertyName}($props[$propertyName]);
|
||||
}
|
||||
}
|
||||
}
|
149
kirby/src/Toolkit/Query.php
Executable file
149
kirby/src/Toolkit/Query.php
Executable file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
/**
|
||||
* The Query class can be used to
|
||||
* query arrays and objects, including their
|
||||
* methods with a very simple string-based syntax.
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
*/
|
||||
class Query
|
||||
{
|
||||
|
||||
/**
|
||||
* The query string
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* Queryable data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Creates a new Query object
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(string $query = null, $data = [])
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the query result if anything
|
||||
* can be found. Otherwise returns null.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function result()
|
||||
{
|
||||
if (empty($this->query) === true) {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
$parts = $this->parts($this->query);
|
||||
$data = $this->data;
|
||||
$value = null;
|
||||
|
||||
while (count($parts)) {
|
||||
$part = array_shift($parts);
|
||||
$info = $this->info($part);
|
||||
$method = $info['method'];
|
||||
$value = null;
|
||||
|
||||
if (is_array($data)) {
|
||||
$value = $data[$method] ?? null;
|
||||
} elseif (is_object($data)) {
|
||||
if (method_exists($data, $method) || method_exists($data, '__call')) {
|
||||
$value = $data->$method(...$info['args']);
|
||||
}
|
||||
} elseif (is_scalar($data)) {
|
||||
return $data;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value) || is_object($value)) {
|
||||
$data = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks the query string down into its components
|
||||
*
|
||||
* @param string $token
|
||||
* @return array
|
||||
*/
|
||||
protected function parts(string $token): array
|
||||
{
|
||||
$token = trim($token);
|
||||
$token = preg_replace_callback('!\((.*?)\)!', function ($match) {
|
||||
return '(' . str_replace('.', '@@@', $match[1]) . ')';
|
||||
}, $token);
|
||||
|
||||
$parts = explode('.', $token);
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes each part of the query string and
|
||||
* extracts methods and method arguments.
|
||||
*
|
||||
* @param string $token
|
||||
* @return array
|
||||
*/
|
||||
protected function info(string $token): array
|
||||
{
|
||||
$args = [];
|
||||
$method = preg_replace_callback('!\((.*?)\)!', function ($match) use (&$args) {
|
||||
$args = array_map(function ($arg) {
|
||||
$arg = trim($arg);
|
||||
$arg = str_replace('@@@', '.', $arg);
|
||||
|
||||
if (substr($arg, 0, 1) === '"') {
|
||||
return trim($arg, '"');
|
||||
}
|
||||
|
||||
if (substr($arg, 0, 1) === '\'') {
|
||||
return trim($arg, '\'');
|
||||
}
|
||||
|
||||
switch ($arg) {
|
||||
case 'null':
|
||||
return null;
|
||||
case 'false':
|
||||
return false;
|
||||
case 'true':
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_numeric($arg) === true) {
|
||||
return (float)$arg;
|
||||
}
|
||||
|
||||
return $arg;
|
||||
}, str_getcsv($match[1], ','));
|
||||
}, $token);
|
||||
|
||||
return [
|
||||
'method' => $method,
|
||||
'args' => $args
|
||||
];
|
||||
}
|
||||
}
|
74
kirby/src/Toolkit/Silo.php
Executable file
74
kirby/src/Toolkit/Silo.php
Executable file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
/**
|
||||
* The Silo class is a core class to handle
|
||||
* setting, getting and removing static data of
|
||||
* a singleton.
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
class Silo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $data = [];
|
||||
|
||||
/**
|
||||
* Setter for new data.
|
||||
*
|
||||
* @param string|array $key
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
public static function set($key, $value = null): array
|
||||
{
|
||||
if (is_array($key) === true) {
|
||||
return static::$data = array_merge(static::$data, $key);
|
||||
} else {
|
||||
static::$data[$key] = $value;
|
||||
return static::$data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get($key = null, $default = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return static::$data;
|
||||
}
|
||||
|
||||
return A::get(static::$data, $key, $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an item from the data array
|
||||
*
|
||||
* @param string|null $key
|
||||
* @return array
|
||||
*/
|
||||
public static function remove(string $key = null): array
|
||||
{
|
||||
// reset the entire array
|
||||
if ($key === true) {
|
||||
return static::$data = [];
|
||||
}
|
||||
|
||||
// unset a single key
|
||||
unset(static::$data[$key]);
|
||||
|
||||
// return the array without the removed key
|
||||
return static::$data;
|
||||
}
|
||||
}
|
957
kirby/src/Toolkit/Str.php
Executable file
957
kirby/src/Toolkit/Str.php
Executable file
@@ -0,0 +1,957 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A set of handy string methods
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class Str
|
||||
{
|
||||
|
||||
/**
|
||||
* Ascii translation table
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $ascii = [
|
||||
'/À|Á|Â|Ã|Å|Ǻ|Ā|Ă|Ą|Ǎ|Ä|A/' => 'A',
|
||||
'/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|æ|ǽ|ä|a/' => 'a',
|
||||
'/Б/' => 'B',
|
||||
'/б/' => 'b',
|
||||
'/Ç|Ć|Ĉ|Ċ|Č|Ц/' => 'C',
|
||||
'/ç|ć|ĉ|ċ|č|ц/' => 'c',
|
||||
'/Ð|Ď|Đ/' => 'Dj',
|
||||
'/ð|ď|đ/' => 'dj',
|
||||
'/Д/' => 'D',
|
||||
'/д/' => 'd',
|
||||
'/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Е|Ё|Э/' => 'E',
|
||||
'/è|é|ê|ë|ē|ĕ|ė|ę|ě|е|ё|э/' => 'e',
|
||||
'/Ф/' => 'F',
|
||||
'/ƒ|ф/' => 'f',
|
||||
'/Ĝ|Ğ|Ġ|Ģ|Г/' => 'G',
|
||||
'/ĝ|ğ|ġ|ģ|г/' => 'g',
|
||||
'/Ĥ|Ħ|Х/' => 'H',
|
||||
'/ĥ|ħ|х/' => 'h',
|
||||
'/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|И/' => 'I',
|
||||
'/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|и/' => 'i',
|
||||
'/Ĵ|Й/' => 'J',
|
||||
'/ĵ|й/' => 'j',
|
||||
'/Ķ|К/' => 'K',
|
||||
'/ķ|к/' => 'k',
|
||||
'/Ĺ|Ļ|Ľ|Ŀ|Ł|Л/' => 'L',
|
||||
'/ĺ|ļ|ľ|ŀ|ł|л/' => 'l',
|
||||
'/М/' => 'M',
|
||||
'/м/' => 'm',
|
||||
'/Ñ|Ń|Ņ|Ň|Н/' => 'N',
|
||||
'/ñ|ń|ņ|ň|ʼn|н/' => 'n',
|
||||
'/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ö|O/' => 'O',
|
||||
'/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ö|o/' => 'o',
|
||||
'/П/' => 'P',
|
||||
'/п/' => 'p',
|
||||
'/Ŕ|Ŗ|Ř|Р/' => 'R',
|
||||
'/ŕ|ŗ|ř|р/' => 'r',
|
||||
'/Ś|Ŝ|Ş|Ș|Š|С/' => 'S',
|
||||
'/ś|ŝ|ş|ș|š|ſ|с/' => 's',
|
||||
'/Ţ|Ț|Ť|Ŧ|Т/' => 'T',
|
||||
'/ţ|ț|ť|ŧ|т/' => 't',
|
||||
'/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|У|Ü|U/' => 'U',
|
||||
'/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|у|ü|u/' => 'u',
|
||||
'/В/' => 'V',
|
||||
'/в/' => 'v',
|
||||
'/Ý|Ÿ|Ŷ|Ы/' => 'Y',
|
||||
'/ý|ÿ|ŷ|ы/' => 'y',
|
||||
'/Ŵ/' => 'W',
|
||||
'/ŵ/' => 'w',
|
||||
'/Ź|Ż|Ž|З/' => 'Z',
|
||||
'/ź|ż|ž|з/' => 'z',
|
||||
'/Æ|Ǽ/' => 'AE',
|
||||
'/ß/'=> 'ss',
|
||||
'/IJ/' => 'IJ',
|
||||
'/ij/' => 'ij',
|
||||
'/Œ/' => 'OE',
|
||||
'/Ч/' => 'Ch',
|
||||
'/ч/' => 'ch',
|
||||
'/Ю/' => 'Ju',
|
||||
'/ю/' => 'ju',
|
||||
'/Я/' => 'Ja',
|
||||
'/я/' => 'ja',
|
||||
'/Ш/' => 'Sh',
|
||||
'/ш/' => 'sh',
|
||||
'/Щ/' => 'Shch',
|
||||
'/щ/' => 'shch',
|
||||
'/Ж/' => 'Zh',
|
||||
'/ж/' => 'zh',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default settings for class methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $defaults = [
|
||||
'slug' => [
|
||||
'separator' => '-',
|
||||
'allowed' => 'a-z0-9'
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse accepted values and their quality from an
|
||||
* accept string like an Accept or Accept-Language header
|
||||
*
|
||||
* @param string $input
|
||||
* @return array
|
||||
*/
|
||||
public static function accepted(string $input): array
|
||||
{
|
||||
$items = [];
|
||||
|
||||
// check each type in the Accept header
|
||||
foreach (static::split($input, ',') as $item) {
|
||||
$parts = static::split($item, ';');
|
||||
$value = A::first($parts); // $parts now only contains params
|
||||
$quality = 1;
|
||||
|
||||
// check for the q param ("quality" of the type)
|
||||
foreach ($parts as $param) {
|
||||
$param = static::split($param, '=');
|
||||
if (A::get($param, 0) === 'q' && !empty($param[1])) {
|
||||
$quality = $param[1];
|
||||
}
|
||||
}
|
||||
|
||||
$items[$quality][] = $value;
|
||||
}
|
||||
|
||||
// sort items by quality
|
||||
krsort($items);
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($items as $quality => $values) {
|
||||
foreach ($values as $value) {
|
||||
$result[] = [
|
||||
'quality' => $quality,
|
||||
'value' => $value
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rest of the string after the given character
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @param bool $caseInsensitive
|
||||
* @return string
|
||||
*/
|
||||
public static function after(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
$position = static::position($string, $needle, $caseInsensitive);
|
||||
|
||||
if ($position === false) {
|
||||
return false;
|
||||
} else {
|
||||
return static::substr($string, $position + static::length($needle));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to 7-bit ASCII.
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function ascii(string $string): string
|
||||
{
|
||||
$foreign = static::$ascii;
|
||||
$string = preg_replace(array_keys($foreign), array_values($foreign), $string);
|
||||
return preg_replace('/[^\x09\x0A\x0D\x20-\x7E]/', '', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the beginning of a string before the given character
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @param bool $caseInsensitive
|
||||
* @return string
|
||||
*/
|
||||
public static function before(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
$position = static::position($string, $needle, $caseInsensitive);
|
||||
|
||||
if ($position === false) {
|
||||
return false;
|
||||
} else {
|
||||
return static::substr($string, 0, $position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns everything between two strings from the first occurrence of a given string
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $start
|
||||
* @param string $end
|
||||
* @return string
|
||||
*/
|
||||
public static function between(string $string = null, string $start, string $end): string
|
||||
{
|
||||
return static::before(static::after($string, $start), $end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a str contains another string
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @param bool $caseInsensitive
|
||||
* @return bool
|
||||
*/
|
||||
public static function contains(string $string = null, string $needle, bool $caseInsensitive = false): bool
|
||||
{
|
||||
return call_user_func($caseInsensitive === true ? 'stristr' : 'strstr', $string, $needle) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to a different encoding
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $targetEncoding
|
||||
* @param string $sourceEncoding (optional)
|
||||
* @return string
|
||||
*/
|
||||
public static function convert($string, $targetEncoding, $sourceEncoding = null)
|
||||
{
|
||||
// detect the source encoding if not passed as third argument
|
||||
if ($sourceEncoding === null) {
|
||||
$sourceEncoding = static::encoding($string);
|
||||
}
|
||||
|
||||
// no need to convert if the target encoding is the same
|
||||
if (strtolower($sourceEncoding) === strtolower($targetEncoding)) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
return iconv($sourceEncoding, $targetEncoding, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a string (used for email addresses)
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(string $string): string
|
||||
{
|
||||
$encoded = '';
|
||||
|
||||
for ($i = 0; $i < static::length($string); $i++) {
|
||||
$char = static::substr($string, $i, 1);
|
||||
list(, $code) = unpack('N', mb_convert_encoding($char, 'UCS-4BE', 'UTF-8'));
|
||||
$encoded .= rand(1, 2) == 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';';
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to detect the string encoding
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function encoding(string $string): string
|
||||
{
|
||||
return mb_detect_encoding($string, 'UTF-8, ISO-8859-1, windows-1251', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string ends with the passed needle
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @param bool $caseInsensitive
|
||||
* @return bool
|
||||
*/
|
||||
public static function endsWith(string $string, string $needle, bool $caseInsensitive = false): bool
|
||||
{
|
||||
if ($needle === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$probe = static::substr($string, -static::length($needle));
|
||||
|
||||
if ($caseInsensitive === true) {
|
||||
$needle = static::lower($needle);
|
||||
$probe = static::lower($probe);
|
||||
}
|
||||
|
||||
return $needle === $probe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an excerpt of a string
|
||||
* It removes all html tags first and then cuts the string
|
||||
* according to the specified number of chars.
|
||||
*
|
||||
* @param string $string The string to be shortened
|
||||
* @param int $chars The final number of characters the string should have
|
||||
* @param boolean $strip True: remove the HTML tags from the string first
|
||||
* @param string $rep The element, which should be added if the string is too long. Ellipsis is the default.
|
||||
* @return string The shortened string
|
||||
*/
|
||||
public static function excerpt($string, $chars = 140, $strip = true, $rep = '…')
|
||||
{
|
||||
if ($strip === true) {
|
||||
$string = strip_tags(str_replace('<', ' <', $string));
|
||||
}
|
||||
|
||||
// replace line breaks with spaces
|
||||
$string = str_replace(PHP_EOL, ' ', trim($string));
|
||||
|
||||
// remove double spaces
|
||||
$string = preg_replace('![ ]{2,}!', ' ', $string);
|
||||
|
||||
if ($chars === 0) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
if (static::length($string) <= $chars) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
return static::substr($string, 0, strrpos(static::substr($string, 0, $chars), ' ')) . ' ' . $rep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rest of the string starting from the given character
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @param bool $caseInsensitive
|
||||
* @return string
|
||||
*/
|
||||
public static function from(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
$position = static::position($string, $needle, $caseInsensitive);
|
||||
|
||||
if ($position === false) {
|
||||
return false;
|
||||
} else {
|
||||
return static::substr($string, $position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given string is a URL
|
||||
*
|
||||
* @param string|null $string
|
||||
* @return boolean
|
||||
*/
|
||||
public static function isURL(string $string = null): bool
|
||||
{
|
||||
return filter_var($string, FILTER_VALIDATE_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to kebab case.
|
||||
*
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
public static function kebab(string $value = null): string
|
||||
{
|
||||
return static::snake($value, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* A UTF-8 safe version of strlen()
|
||||
*
|
||||
* @param string $string
|
||||
* @return int
|
||||
*/
|
||||
public static function length(string $string = null): int
|
||||
{
|
||||
return mb_strlen($string, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* A UTF-8 safe version of strtolower()
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function lower(string $string = null): string
|
||||
{
|
||||
return mb_strtolower($string, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe ltrim alternative
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $trim
|
||||
* @return string
|
||||
*/
|
||||
public static function ltrim(string $string, string $trim = ' '): string
|
||||
{
|
||||
return preg_replace('!^(' . preg_quote($trim) . ')+!', '', $string);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a character pool with various possible combinations
|
||||
*
|
||||
* @param string|array $type
|
||||
* @param boolean $array
|
||||
* @return string|array
|
||||
*/
|
||||
public static function pool($type, bool $array = true)
|
||||
{
|
||||
$pool = [];
|
||||
|
||||
if (is_array($type) === true) {
|
||||
foreach ($type as $t) {
|
||||
$pool = array_merge($pool, static::pool($t));
|
||||
}
|
||||
|
||||
return $pool;
|
||||
} else {
|
||||
switch ($type) {
|
||||
case 'alphaLower':
|
||||
$pool = range('a', 'z');
|
||||
break;
|
||||
case 'alphaUpper':
|
||||
$pool = range('A', 'Z');
|
||||
break;
|
||||
case 'alpha':
|
||||
$pool = static::pool(['alphaLower', 'alphaUpper']);
|
||||
break;
|
||||
case 'num':
|
||||
$pool = range(0, 9);
|
||||
break;
|
||||
case 'alphaNum':
|
||||
$pool = static::pool(['alpha', 'num']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $array ? $pool : implode('', $pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of a needle in a string
|
||||
* if it can be found
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @param bool $caseInsensitive
|
||||
* @return int|bool
|
||||
*/
|
||||
public static function position(string $string, string $needle, bool $caseInsensitive = false)
|
||||
{
|
||||
if ($caseInsensitive === true) {
|
||||
$string = static::lower($string);
|
||||
$needle = static::lower($needle);
|
||||
}
|
||||
|
||||
return mb_strpos($string, $needle, 0, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a string query.
|
||||
* Check out the Query class for more information.
|
||||
*
|
||||
* @param string $query
|
||||
* @param array $data
|
||||
* @return string|null
|
||||
*/
|
||||
public static function query(string $query, array $data = [])
|
||||
{
|
||||
return (new Query($query, $data))->result();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random string that may be used for cryptographic purposes
|
||||
*
|
||||
* @param int $length The length of the random string
|
||||
* @param string $type Pool type (type of allowed characters)
|
||||
* @return string
|
||||
*/
|
||||
public static function random(int $length = null, string $type = 'alphaNum')
|
||||
{
|
||||
if ($length === null) {
|
||||
$length = random_int(5, 10);
|
||||
}
|
||||
|
||||
$pool = static::pool($type, false);
|
||||
|
||||
// catch invalid pools
|
||||
if (!$pool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// regex that matches all characters *not* in the pool of allowed characters
|
||||
$regex = '/[^' . $pool . ']/';
|
||||
|
||||
// collect characters until we have our required length
|
||||
$result = '';
|
||||
|
||||
while (($currentLength = strlen($result)) < $length) {
|
||||
$missing = $length - $currentLength;
|
||||
$bytes = random_bytes($missing);
|
||||
$result .= substr(preg_replace($regex, '', base64_encode($bytes)), 0, $missing);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all or some occurrences of the search string with the replacement string
|
||||
* Extension of the str_replace() function in PHP with an additional $limit parameter
|
||||
*
|
||||
* @param string|array $string String being replaced on (haystack);
|
||||
* can be an array of multiple subject strings
|
||||
* @param string|array $search Value being searched for (needle)
|
||||
* @param string|array $replace Value to replace matches with
|
||||
* @param int|array $limit Maximum possible replacements for each search value;
|
||||
* multiple limits for each search value are supported;
|
||||
* defaults to no limit
|
||||
* @return string|array String with replaced values;
|
||||
* if $string is an array, array of strings
|
||||
*/
|
||||
public static function replace($string, $search, $replace, $limit = -1)
|
||||
{
|
||||
// convert Kirby collections to arrays
|
||||
if (is_a($string, 'Kirby\Toolkit\Collection') === true) {
|
||||
$string = $string->toArray();
|
||||
}
|
||||
|
||||
if (is_a($search, 'Kirby\Toolkit\Collection') === true) {
|
||||
$search = $search->toArray();
|
||||
}
|
||||
|
||||
if (is_a($replace, 'Kirby\Toolkit\Collection') === true) {
|
||||
$replace = $replace->toArray();
|
||||
}
|
||||
|
||||
// without a limit we might as well use the built-in function
|
||||
if ($limit === -1) {
|
||||
return str_replace($search, $replace, $string);
|
||||
}
|
||||
|
||||
// if the limit is zero, the result will be no replacements at all
|
||||
if ($limit === 0) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
// multiple subjects are run separately through this method
|
||||
if (is_array($string) === true) {
|
||||
$result = [];
|
||||
foreach ($string as $s) {
|
||||
$result[] = static::replace($s, $search, $replace, $limit);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
// build an array of replacements
|
||||
// we don't use an associative array because otherwise you couldn't
|
||||
// replace the same string with different replacements
|
||||
$replacements = static::replacements($search, $replace, $limit);
|
||||
|
||||
// run the string and the replacement array through the replacer
|
||||
return static::replaceReplacements($string, $replacements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a replacement array out of dynamic input data
|
||||
* Used for Str::replace()
|
||||
*
|
||||
* @param string|array $search Value being searched for (needle)
|
||||
* @param string|array $replace Value to replace matches with
|
||||
* @param int|array $limit Maximum possible replacements for each search value;
|
||||
* multiple limits for each search value are supported;
|
||||
* defaults to no limit
|
||||
* @return array List of replacement arrays, each with a
|
||||
* 'search', 'replace' and 'limit' attribute
|
||||
*/
|
||||
public static function replacements($search, $replace, $limit): array
|
||||
{
|
||||
$replacements = [];
|
||||
|
||||
if (is_array($search) === true && is_array($replace) === true) {
|
||||
foreach ($search as $i => $s) {
|
||||
// replace with an empty string if no replacement string was defined for this index;
|
||||
// behavior is identical to the official PHP str_replace()
|
||||
$r = $replace[$i] ?? '';
|
||||
|
||||
if (is_array($limit) === true) {
|
||||
// don't apply a limit if no limit was defined for this index
|
||||
$l = $limit[$i] ?? -1;
|
||||
} else {
|
||||
$l = $limit;
|
||||
}
|
||||
|
||||
$replacements[] = ['search' => $s, 'replace' => $r, 'limit' => $l];
|
||||
}
|
||||
} elseif (is_array($search) === true && is_string($replace) === true) {
|
||||
foreach ($search as $i => $s) {
|
||||
if (is_array($limit) === true) {
|
||||
// don't apply a limit if no limit was defined for this index
|
||||
$l = $limit[$i] ?? -1;
|
||||
} else {
|
||||
$l = $limit;
|
||||
}
|
||||
|
||||
$replacements[] = ['search' => $s, 'replace' => $replace, 'limit' => $l];
|
||||
}
|
||||
} elseif (is_string($search) === true && is_string($replace) === true && is_int($limit) === true) {
|
||||
$replacements[] = compact('search', 'replace', 'limit');
|
||||
} else {
|
||||
throw new Exception('Invalid combination of $search, $replace and $limit params.');
|
||||
}
|
||||
|
||||
return $replacements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a replacement array and processes the replacements
|
||||
* Used for Str::replace()
|
||||
*
|
||||
* @param string $string String being replaced on (haystack)
|
||||
* @param array $replacements Replacement array from Str::replacements()
|
||||
* @return string String with replaced values
|
||||
*/
|
||||
public static function replaceReplacements(string $string, array $replacements): string
|
||||
{
|
||||
// replace in the order of the replacements
|
||||
// behavior is identical to the official PHP str_replace()
|
||||
foreach ($replacements as $replacement) {
|
||||
if (is_int($replacement['limit']) === false) {
|
||||
throw new Exception('Invalid limit "' . $replacement['limit'] . '".');
|
||||
} elseif ($replacement['limit'] === -1) {
|
||||
|
||||
// no limit, we don't need our special replacement routine
|
||||
$string = str_replace($replacement['search'], $replacement['replace'], $string);
|
||||
} elseif ($replacement['limit'] > 0) {
|
||||
|
||||
// limit given, only replace for $replacement['limit'] times per replacement
|
||||
$position = -1;
|
||||
|
||||
for ($i = 0; $i < $replacement['limit']; $i++) {
|
||||
$position = strpos($string, $replacement['search'], $position + 1);
|
||||
|
||||
if (is_int($position) === true) {
|
||||
$string = substr_replace($string, $replacement['replace'], $position, strlen($replacement['search']));
|
||||
// adapt $pos to the now changed offset
|
||||
$position = $position + strlen($replacement['replace']) - strlen($replacement['search']);
|
||||
} else {
|
||||
// no more match in the string
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe rtrim alternative
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $trim
|
||||
* @return string
|
||||
*/
|
||||
public static function rtrim(string $string, string $trim = ' '): string
|
||||
{
|
||||
return preg_replace('!(' . preg_quote($trim) . ')+$!', '', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortens a string and adds an ellipsis if the string is too long
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* echo Str::short('This is a very, very, very long string', 10);
|
||||
* // output: This is a…
|
||||
*
|
||||
* echo Str::short('This is a very, very, very long string', 10, '####');
|
||||
* // output: This i####
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string The string to be shortened
|
||||
* @param int $length The final number of characters the
|
||||
* string should have
|
||||
* @param string $appendix The element, which should be added if the
|
||||
* string is too long. Ellipsis is the default.
|
||||
* @return string The shortened string
|
||||
*/
|
||||
public static function short(string $string = null, int $length = 0, string $appendix = '…'): ?string
|
||||
{
|
||||
if ($length === 0) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
if (static::length($string) <= $length) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
return static::substr($string, 0, $length) . $appendix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to a safe version to be used in a URL
|
||||
*
|
||||
* @param string $string The unsafe string
|
||||
* @param string $separator To be used instead of space and
|
||||
* other non-word characters.
|
||||
* @param string $allowed List of all allowed characters (regex)
|
||||
* @return string The safe string
|
||||
*/
|
||||
public static function slug(string $string = null, string $separator = null, string $allowed = null): string
|
||||
{
|
||||
$separator = $separator ?? static::$defaults['slug']['separator'];
|
||||
$allowed = $allowed ?? static::$defaults['slug']['allowed'];
|
||||
|
||||
$string = trim($string);
|
||||
$string = static::lower($string);
|
||||
$string = static::ascii($string);
|
||||
|
||||
// replace spaces with simple dashes
|
||||
$string = preg_replace('![^' . $allowed . ']!i', $separator, $string);
|
||||
|
||||
if (strlen($separator) > 0) {
|
||||
// remove double separators
|
||||
$string = preg_replace('![' . preg_quote($separator) . ']{2,}!', $separator, $string);
|
||||
}
|
||||
|
||||
// replace slashes with dashes
|
||||
$string = str_replace('/', $separator, $string);
|
||||
|
||||
// trim leading and trailing non-word-chars
|
||||
$string = preg_replace('!^[^a-z0-9]+!', '', $string);
|
||||
$string = preg_replace('![^a-z0-9]+$!', '', $string);
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string to snake case.
|
||||
*
|
||||
* @param string $value
|
||||
* @param string $delimiter
|
||||
* @return string
|
||||
*/
|
||||
public static function snake(string $value = null, string $delimiter = '_'): string
|
||||
{
|
||||
if (!ctype_lower($value)) {
|
||||
$value = preg_replace('/\s+/u', '', ucwords($value));
|
||||
$value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value));
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Better alternative for explode()
|
||||
* It takes care of removing empty values
|
||||
* and it has a built-in way to skip values
|
||||
* which are too short.
|
||||
*
|
||||
* @param string $string The string to split
|
||||
* @param string $separator The string to split by
|
||||
* @param int $length The min length of values.
|
||||
* @return array An array of found values
|
||||
*/
|
||||
public static function split($string, string $separator = ',', int $length = 1): array
|
||||
{
|
||||
if (is_array($string) === true) {
|
||||
return $string;
|
||||
}
|
||||
|
||||
$string = trim((string)$string, $separator);
|
||||
$parts = explode($separator, $string);
|
||||
$out = [];
|
||||
|
||||
foreach ($parts as $p) {
|
||||
$p = trim($p);
|
||||
if (static::length($p) > 0 && static::length($p) >= $length) {
|
||||
$out[] = $p;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string starts with the passed needle
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @param bool $caseInsensitive
|
||||
* @return bool
|
||||
*/
|
||||
public static function startsWith(string $string, string $needle, bool $caseInsensitive = false): bool
|
||||
{
|
||||
if ($needle === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return static::position($string, $needle, $caseInsensitive) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* A UTF-8 safe version of substr()
|
||||
*
|
||||
* @param string $string
|
||||
* @param int $start
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public static function substr(string $string = null, int $start = 0, int $length = null): string
|
||||
{
|
||||
return mb_substr($string, $start, $length, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces placeholders in string with value from array
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* echo Str::template('From {{ b }} to {{ a }}', ['a' => 'there', 'b' => 'here']);
|
||||
* // output: From here to there
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string The string with placeholders
|
||||
* @param array $data Associative array with placeholders as
|
||||
* keys and replacements as values
|
||||
* @param string $fallback A fallback if a token does not have any matches
|
||||
* @return string The filled-in string
|
||||
*/
|
||||
public static function template(string $string = null, array $data = [], string $fallback = null, string $start = '{{', string $end = '}}'): string
|
||||
{
|
||||
return preg_replace_callback('!' . $start . '(.*?)' . $end . '!', function ($match) use ($data, $fallback) {
|
||||
$query = trim($match[1]);
|
||||
if (strpos($query, '.') !== false) {
|
||||
return (new Query($match[1], $data))->result() ?? $fallback;
|
||||
}
|
||||
return $data[$query] ?? $fallback;
|
||||
}, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe trim alternative
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $trim
|
||||
* @return string
|
||||
*/
|
||||
public static function trim(string $string, string $trim = ' '): string
|
||||
{
|
||||
return static::rtrim(static::ltrim($string, $trim), $trim);
|
||||
}
|
||||
|
||||
/**
|
||||
* A UTF-8 safe version of ucfirst()
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function ucfirst(string $string = null): string
|
||||
{
|
||||
return static::upper(static::substr($string, 0, 1)) . static::lower(static::substr($string, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* A UTF-8 safe version of ucwords()
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function ucwords(string $string = null): string
|
||||
{
|
||||
return mb_convert_case($string, MB_CASE_TITLE, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all html tags and encoded chars from a string
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* echo str::unhtml('some <em>crazy</em> stuff');
|
||||
* // output: some uber crazy stuff
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string The html string
|
||||
*/
|
||||
public static function unhtml(string $string = null): string
|
||||
{
|
||||
return Html::decode($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the beginning of a string until the given character
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $needle
|
||||
* @param bool $caseInsensitive
|
||||
* @return string
|
||||
*/
|
||||
public static function until(string $string, string $needle, bool $caseInsensitive = false): string
|
||||
{
|
||||
$position = static::position($string, $needle, $caseInsensitive);
|
||||
|
||||
if ($position === false) {
|
||||
return false;
|
||||
} else {
|
||||
return static::substr($string, 0, $position + static::length($needle));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A UTF-8 safe version of strotoupper()
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function upper(string $string = null): string
|
||||
{
|
||||
return mb_strtoupper($string, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* The widont function makes sure that there are no
|
||||
* typographical widows at the end of a paragraph –
|
||||
* that's a single word in the last line
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function widont(string $string = null): string
|
||||
{
|
||||
return preg_replace_callback('|([^\s])\s+([^\s]+)\s*$|u', function ($matches) {
|
||||
if (static::contains($matches[2], '-')) {
|
||||
return $matches[1] . ' ' . str_replace('-', '‑', $matches[2]);
|
||||
} else {
|
||||
return $matches[1] . ' ' . $matches[2];
|
||||
}
|
||||
}, $string);
|
||||
}
|
||||
}
|
53
kirby/src/Toolkit/Tpl.php
Executable file
53
kirby/src/Toolkit/Tpl.php
Executable file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Simple PHP template engine
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class Tpl
|
||||
{
|
||||
|
||||
/**
|
||||
* Renders the template
|
||||
*
|
||||
* @param string $__file
|
||||
* @param array $__data
|
||||
* @return string
|
||||
*/
|
||||
public static function load(string $__file = null, array $__data = []): string
|
||||
{
|
||||
if (file_exists($__file) === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$exception = null;
|
||||
|
||||
ob_start();
|
||||
extract($__data);
|
||||
|
||||
try {
|
||||
require $__file;
|
||||
} catch (Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
if ($exception === null) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
467
kirby/src/Toolkit/V.php
Executable file
467
kirby/src/Toolkit/V.php
Executable file
@@ -0,0 +1,467 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\Str;
|
||||
use ReflectionFunction;
|
||||
|
||||
/**
|
||||
* A set of validator methods
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class V
|
||||
{
|
||||
|
||||
/**
|
||||
* An array with all installed validators
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $validators = [];
|
||||
|
||||
/**
|
||||
* Validates the given input with all passed rules
|
||||
* and returns an array with all error messages.
|
||||
* The array will be empty if the input is valid
|
||||
*
|
||||
* @param mixed $input
|
||||
* @param array $rules
|
||||
* @param array $messages
|
||||
* @return array
|
||||
*/
|
||||
public static function errors($input, array $rules, $messages = []): array
|
||||
{
|
||||
$errors = static::value($input, $rules, $messages, false);
|
||||
|
||||
return $errors === true ? [] : $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a useful error message for the given validator
|
||||
* and the arguments. This is used mainly internally
|
||||
* to create error messages
|
||||
*
|
||||
* @param string $validatorName
|
||||
* @param mixed ...$params
|
||||
* @return string|null
|
||||
*/
|
||||
public static function message(string $validatorName, ...$params): ?string
|
||||
{
|
||||
$validatorName = strtolower($validatorName);
|
||||
$translationKey = 'error.validation.' . $validatorName;
|
||||
$validators = array_change_key_case(static::$validators);
|
||||
$validator = $validators[$validatorName] ?? null;
|
||||
|
||||
if ($validator === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionFunction($validator);
|
||||
$arguments = [];
|
||||
|
||||
|
||||
foreach ($reflection->getParameters() as $index => $parameter) {
|
||||
$value = $params[$index] ?? null;
|
||||
|
||||
if (is_array($value) === true) {
|
||||
$value = implode(', ', $value);
|
||||
}
|
||||
|
||||
$arguments[$parameter->getName()] = $value;
|
||||
}
|
||||
|
||||
return I18n::template($translationKey, 'The "' . $validatorName . '" validation failed', $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of all validators
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function validators(): array
|
||||
{
|
||||
return static::$validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single value against
|
||||
* a set of rules, using all registered
|
||||
* validators
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array $rules
|
||||
* @param array $messages
|
||||
* @param boolean $fail
|
||||
* @return boolean|array
|
||||
*/
|
||||
public static function value($value, array $rules, array $messages = [], bool $fail = true)
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $validatorName => $validatorOptions) {
|
||||
if (is_int($validatorName)) {
|
||||
$validatorName = $validatorOptions;
|
||||
$validatorOptions = [];
|
||||
}
|
||||
|
||||
if (is_array($validatorOptions) === false) {
|
||||
$validatorOptions = [$validatorOptions];
|
||||
}
|
||||
|
||||
$validatorName = strtolower($validatorName);
|
||||
|
||||
if (static::$validatorName($value, ...$validatorOptions) === false) {
|
||||
$message = $messages[$validatorName] ?? static::message($validatorName, $value, ...$validatorOptions);
|
||||
$errors[$validatorName] = $message;
|
||||
|
||||
if ($fail === true) {
|
||||
throw new Exception($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return empty($errors) === true ? true : $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an input array against
|
||||
* a set of rules, using all registered
|
||||
* validators
|
||||
*
|
||||
* @param array $input
|
||||
* @param array $rules
|
||||
* @return boolean
|
||||
*/
|
||||
public static function input(array $input, array $rules): bool
|
||||
{
|
||||
foreach ($rules as $fieldName => $fieldRules) {
|
||||
$fieldValue = $input[$fieldName] ?? null;
|
||||
|
||||
// first check for required fields
|
||||
if (($fieldRules['required'] ?? false) === true && $fieldValue === null) {
|
||||
throw new Exception(sprintf('The "%s" field is missing', $fieldName));
|
||||
}
|
||||
|
||||
// remove the required rule
|
||||
unset($fieldRules['required']);
|
||||
|
||||
// skip validation for empty fields
|
||||
if ($fieldValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
V::value($fieldValue, $fieldRules);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception(sprintf($e->getMessage() . ' for field "%s"', $fieldName));
|
||||
}
|
||||
|
||||
static::value($fieldValue, $fieldRules);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls an installed validator and passes all arguments
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return boolean
|
||||
*/
|
||||
public static function __callStatic(string $method, array $arguments): bool
|
||||
{
|
||||
$method = strtolower($method);
|
||||
$validators = array_change_key_case(static::$validators);
|
||||
|
||||
// check for missing validators
|
||||
if (isset($validators[$method]) === false) {
|
||||
throw new Exception('The validator does not exist: ' . $method);
|
||||
}
|
||||
|
||||
return call_user_func_array($validators[$method], $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default set of validators
|
||||
*/
|
||||
V::$validators = [
|
||||
/**
|
||||
* Valid: `'yes' | true | 1 | 'on'`
|
||||
*/
|
||||
'accepted' => function ($value): bool {
|
||||
return V::in($value, [1, true, 'yes', 'true', '1', 'on'], true) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Valid: `a-z | A-Z`
|
||||
*/
|
||||
'alpha' => function ($value): bool {
|
||||
return V::match($value, '/^([a-z])+$/i') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Valid: `a-z | A-Z | 0-9`
|
||||
*/
|
||||
'alphanum' => function ($value): bool {
|
||||
return V::match($value, '/^[a-z0-9]+$/i') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for numbers within the given range
|
||||
*/
|
||||
'between' => function ($value, $min, $max): bool {
|
||||
return V::min($value, $min) === true &&
|
||||
V::max($value, $max) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the given string contains the given value
|
||||
*/
|
||||
'contains' => function ($value, $needle): bool {
|
||||
return Str::contains($value, $needle);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid date
|
||||
*/
|
||||
'date' => function ($value): bool {
|
||||
$date = date_parse($value);
|
||||
return ($date !== false &&
|
||||
$date['error_count'] === 0 &&
|
||||
$date['warning_count'] === 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Valid: `'no' | false | 0 | 'off'`
|
||||
*/
|
||||
'denied' => function ($value): bool {
|
||||
return V::in($value, [0, false, 'no', 'false', '0', 'off'], true) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a value, which does not equal the given value
|
||||
*/
|
||||
'different' => function ($value, $other, $strict = false): bool {
|
||||
if ($strict === true) {
|
||||
return $value !== $other;
|
||||
}
|
||||
return $value != $other;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for valid email addresses
|
||||
*/
|
||||
'email' => function ($value): bool {
|
||||
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the given string ends with the given value
|
||||
*/
|
||||
'endsWith' => function (string $value, string $end): bool {
|
||||
return Str::endsWith($value, $end);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid filename
|
||||
*/
|
||||
'filename' => function ($value): bool {
|
||||
return V::match($value, '/^[a-z0-9@._-]+$/i') === true &&
|
||||
V::min($value, 2) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value exists in a list of given values
|
||||
*/
|
||||
'in' => function ($value, array $in, bool $strict = false): bool {
|
||||
return in_array($value, $in, $strict) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid integer
|
||||
*/
|
||||
'integer' => function ($value, bool $strict = false): bool {
|
||||
if ($strict === true) {
|
||||
return is_int($value) === true;
|
||||
}
|
||||
return filter_var($value, FILTER_VALIDATE_INT) !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid IP address
|
||||
*/
|
||||
'ip' => function ($value): bool {
|
||||
return filter_var($value, FILTER_VALIDATE_IP) !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value is lower than the second value
|
||||
*/
|
||||
'less' => function ($value, float $max): bool {
|
||||
return V::size($value, $max, '<') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value matches the given regular expression
|
||||
*/
|
||||
'match' => function ($value, string $pattern): bool {
|
||||
return preg_match($pattern, $value) !== 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value does not exceed the maximum value
|
||||
*/
|
||||
'max' => function ($value, float $max): bool {
|
||||
return V::size($value, $max, '<=') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value is higher than the minimum value
|
||||
*/
|
||||
'min' => function ($value, float $min): bool {
|
||||
return V::size($value, $min, '>=') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the number of characters in the value equals or is below the given maximum
|
||||
*/
|
||||
'maxLength' => function (string $value = null, $max): bool {
|
||||
return Str::length(trim($value)) <= $max;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the number of characters in the value equals or is greater than the given minimum
|
||||
*/
|
||||
'minLength' => function (string $value = null, $min): bool {
|
||||
return Str::length(trim($value)) >= $min;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the number of words in the value equals or is below the given maximum
|
||||
*/
|
||||
'maxWords' => function (string $value = null, $max): bool {
|
||||
return V::max(explode(' ', trim($value)), $max) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the number of words in the value equals or is below the given maximum
|
||||
*/
|
||||
'minWords' => function (string $value = null, $min): bool {
|
||||
return V::min(explode(' ', trim($value)), $min) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the first value is higher than the second value
|
||||
*/
|
||||
'more' => function ($value, float $min): bool {
|
||||
return V::size($value, $min, '>') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the given string does not contain the second value
|
||||
*/
|
||||
'notContains' => function ($value, $needle): bool {
|
||||
return V::contains($value, $needle) === false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the given value is not in the given list of values
|
||||
*/
|
||||
'notIn' => function ($value, $notIn): bool {
|
||||
return V::in($value, $notIn) === false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid number / numeric value (float, int, double)
|
||||
*/
|
||||
'num' => function ($value): bool {
|
||||
return is_numeric($value) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value is present in the given array
|
||||
*/
|
||||
'required' => function ($key, array $array): bool {
|
||||
return isset($array[$key]) === true &&
|
||||
V::notIn($array[$key], [null, '', []]) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the first value equals the second value
|
||||
*/
|
||||
'same' => function ($value, $other, bool $strict = false): bool {
|
||||
if ($strict === true) {
|
||||
return $value === $other;
|
||||
}
|
||||
return $value == $other;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the value has the given size
|
||||
*/
|
||||
'size' => function ($value, $size, $operator = '=='): bool {
|
||||
if (is_numeric($value) === true) {
|
||||
$count = $value;
|
||||
} elseif (is_string($value) === true) {
|
||||
$count = Str::length(trim($value));
|
||||
} elseif (is_array($value) === true) {
|
||||
$count = count($value);
|
||||
} elseif (is_object($value) === true) {
|
||||
if ($value instanceof \Countable) {
|
||||
$count = count($value);
|
||||
} elseif (method_exists($value, 'count') === true) {
|
||||
$count = $value->count();
|
||||
} else {
|
||||
throw new Exception('$value is an uncountable object');
|
||||
}
|
||||
} else {
|
||||
throw new Exception('$value is of type without size');
|
||||
}
|
||||
|
||||
switch ($operator) {
|
||||
case '<':
|
||||
return $count < $size;
|
||||
case '>':
|
||||
return $count > $size;
|
||||
case '<=':
|
||||
return $count <= $size;
|
||||
case '>=':
|
||||
return $count >= $size;
|
||||
default:
|
||||
return $count == $size;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the string starts with the given start value
|
||||
*/
|
||||
'startsWith' => function (string $value, string $start): bool {
|
||||
return Str::startsWith($value, $start);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for valid time
|
||||
*/
|
||||
'time' => function ($value): bool {
|
||||
return V::date($value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid Url
|
||||
*/
|
||||
'url' => function ($value): bool {
|
||||
// In search for the perfect regular expression: https://mathiasbynens.be/demo/url-regex
|
||||
$regex = '_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iu';
|
||||
return preg_match($regex, $value) !== 0;
|
||||
}
|
||||
];
|
139
kirby/src/Toolkit/View.php
Executable file
139
kirby/src/Toolkit/View.php
Executable file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Simple PHP view engine
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class View
|
||||
{
|
||||
|
||||
/**
|
||||
* The absolute path to the view file
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $file;
|
||||
|
||||
/**
|
||||
* The view data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* Creates a new view object
|
||||
*
|
||||
* @param string $file
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(string $file, array $data = [])
|
||||
{
|
||||
$this->file = $file;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view's data array
|
||||
* without globals.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the template file exists
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
return file_exists($this->file()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view file
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function file()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error message for the missing view exception
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function missingViewMessage(): string
|
||||
{
|
||||
return 'The view does not exist: ' . $this->file();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the view
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
if ($this->exists() === false) {
|
||||
throw new Exception($this->missingViewMessage());
|
||||
}
|
||||
|
||||
$exception = null;
|
||||
|
||||
ob_start();
|
||||
extract($this->data());
|
||||
|
||||
try {
|
||||
require $this->file();
|
||||
} catch (Throwable $e) {
|
||||
$exception = $e;
|
||||
}
|
||||
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
if ($exception === null) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for View::render()
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic string converter to enable
|
||||
* converting view objects to string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
}
|
246
kirby/src/Toolkit/Xml.php
Executable file
246
kirby/src/Toolkit/Xml.php
Executable file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
/**
|
||||
* XML parser and creator Class
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license http://www.opensource.org/licenses/mit-license.php MIT License
|
||||
*/
|
||||
class Xml
|
||||
{
|
||||
|
||||
/**
|
||||
* Conversion table for html entities
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $entities = [
|
||||
' ' => ' ', '¡' => '¡', '¢' => '¢', '£' => '£', '¤' => '¤', '¥' => '¥', '¦' => '¦', '§' => '§',
|
||||
'¨' => '¨', '©' => '©', 'ª' => 'ª', '«' => '«', '¬' => '¬', '­' => '­', '®' => '®', '¯' => '¯',
|
||||
'°' => '°', '±' => '±', '²' => '²', '³' => '³', '´' => '´', 'µ' => 'µ', '¶' => '¶', '·' => '·',
|
||||
'¸' => '¸', '¹' => '¹', 'º' => 'º', '»' => '»', '¼' => '¼', '½' => '½', '¾' => '¾', '¿' => '¿',
|
||||
'À' => 'À', 'Á' => 'Á', 'Â' => 'Â', 'Ã' => 'Ã', 'Ä' => 'Ä', 'Å' => 'Å', 'Æ' => 'Æ', 'Ç' => 'Ç',
|
||||
'È' => 'È', 'É' => 'É', 'Ê' => 'Ê', 'Ë' => 'Ë', 'Ì' => 'Ì', 'Í' => 'Í', 'Î' => 'Î', 'Ï' => 'Ï',
|
||||
'Ð' => 'Ð', 'Ñ' => 'Ñ', 'Ò' => 'Ò', 'Ó' => 'Ó', 'Ô' => 'Ô', 'Õ' => 'Õ', 'Ö' => 'Ö', '×' => '×',
|
||||
'Ø' => 'Ø', 'Ù' => 'Ù', 'Ú' => 'Ú', 'Û' => 'Û', 'Ü' => 'Ü', 'Ý' => 'Ý', 'Þ' => 'Þ', 'ß' => 'ß',
|
||||
'à' => 'à', 'á' => 'á', 'â' => 'â', 'ã' => 'ã', 'ä' => 'ä', 'å' => 'å', 'æ' => 'æ', 'ç' => 'ç',
|
||||
'è' => 'è', 'é' => 'é', 'ê' => 'ê', 'ë' => 'ë', 'ì' => 'ì', 'í' => 'í', 'î' => 'î', 'ï' => 'ï',
|
||||
'ð' => 'ð', 'ñ' => 'ñ', 'ò' => 'ò', 'ó' => 'ó', 'ô' => 'ô', 'õ' => 'õ', 'ö' => 'ö', '÷' => '÷',
|
||||
'ø' => 'ø', 'ù' => 'ù', 'ú' => 'ú', 'û' => 'û', 'ü' => 'ü', 'ý' => 'ý', 'þ' => 'þ', 'ÿ' => 'ÿ',
|
||||
'ƒ' => 'ƒ', 'Α' => 'Α', 'Β' => 'Β', 'Γ' => 'Γ', 'Δ' => 'Δ', 'Ε' => 'Ε', 'Ζ' => 'Ζ', 'Η' => 'Η',
|
||||
'Θ' => 'Θ', 'Ι' => 'Ι', 'Κ' => 'Κ', 'Λ' => 'Λ', 'Μ' => 'Μ', 'Ν' => 'Ν', 'Ξ' => 'Ξ', 'Ο' => 'Ο',
|
||||
'Π' => 'Π', 'Ρ' => 'Ρ', 'Σ' => 'Σ', 'Τ' => 'Τ', 'Υ' => 'Υ', 'Φ' => 'Φ', 'Χ' => 'Χ', 'Ψ' => 'Ψ',
|
||||
'Ω' => 'Ω', 'α' => 'α', 'β' => 'β', 'γ' => 'γ', 'δ' => 'δ', 'ε' => 'ε', 'ζ' => 'ζ', 'η' => 'η',
|
||||
'θ' => 'θ', 'ι' => 'ι', 'κ' => 'κ', 'λ' => 'λ', 'μ' => 'μ', 'ν' => 'ν', 'ξ' => 'ξ', 'ο' => 'ο',
|
||||
'π' => 'π', 'ρ' => 'ρ', 'ς' => 'ς', 'σ' => 'σ', 'τ' => 'τ', 'υ' => 'υ', 'φ' => 'φ', 'χ' => 'χ',
|
||||
'ψ' => 'ψ', 'ω' => 'ω', 'ϑ' => 'ϑ', 'ϒ' => 'ϒ', 'ϖ' => 'ϖ', '•' => '•', '…' => '…', '′' => '′',
|
||||
'″' => '″', '‾' => '‾', '⁄' => '⁄', '℘' => '℘', 'ℑ' => 'ℑ', 'ℜ' => 'ℜ', '™' => '™', 'ℵ' => 'ℵ',
|
||||
'←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '↔' => '↔', '↵' => '↵', '⇐' => '⇐', '⇑' => '⇑',
|
||||
'⇒' => '⇒', '⇓' => '⇓', '⇔' => '⇔', '∀' => '∀', '∂' => '∂', '∃' => '∃', '∅' => '∅', '∇' => '∇',
|
||||
'∈' => '∈', '∉' => '∉', '∋' => '∋', '∏' => '∏', '∑' => '∑', '−' => '−', '∗' => '∗', '√' => '√',
|
||||
'∝' => '∝', '∞' => '∞', '∠' => '∠', '∧' => '∧', '∨' => '∨', '∩' => '∩', '∪' => '∪', '∫' => '∫',
|
||||
'∴' => '∴', '∼' => '∼', '≅' => '≅', '≈' => '≈', '≠' => '≠', '≡' => '≡', '≤' => '≤', '≥' => '≥',
|
||||
'⊂' => '⊂', '⊃' => '⊃', '⊄' => '⊄', '⊆' => '⊆', '⊇' => '⊇', '⊕' => '⊕', '⊗' => '⊗', '⊥' => '⊥',
|
||||
'⋅' => '⋅', '⌈' => '⌈', '⌉' => '⌉', '⌊' => '⌊', '⌋' => '⌋', '⟨' => '〈', '⟩' => '〉', '◊' => '◊',
|
||||
'♠' => '♠', '♣' => '♣', '♥' => '♥', '♦' => '♦', '"' => '"', '&' => '&', '<' => '<', '>' => '>', 'Œ' => 'Œ',
|
||||
'œ' => 'œ', 'Š' => 'Š', 'š' => 'š', 'Ÿ' => 'Ÿ', 'ˆ' => 'ˆ', '˜' => '˜', ' ' => ' ', ' ' => ' ',
|
||||
' ' => ' ', '‌' => '‌', '‍' => '‍', '‎' => '‎', '‏' => '‏', '–' => '–', '—' => '—', '‘' => '‘',
|
||||
'’' => '’', '‚' => '‚', '“' => '“', '”' => '”', '„' => '„', '†' => '†', '‡' => '‡', '‰' => '‰',
|
||||
'‹' => '‹', '›' => '›', '€' => '€'
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates an XML string from an array
|
||||
*
|
||||
* @param string $props The source array
|
||||
* @param string $name The name of the root element
|
||||
* @param boolean $head Include the xml declaration head or not
|
||||
* @param int $level The indendation level
|
||||
* @return string The XML string
|
||||
*/
|
||||
public static function create($props, string $name = 'root', bool $head = true, $level = 0): string
|
||||
{
|
||||
$attributes = $props['@attributes'] ?? null;
|
||||
$value = $props['@value'] ?? null;
|
||||
$children = $props;
|
||||
$indent = str_repeat(' ', $level);
|
||||
$nextLevel = $level + 1;
|
||||
|
||||
if (is_array($children) === true) {
|
||||
unset($children['@attributes'], $children['@value']);
|
||||
|
||||
$childTags = [];
|
||||
|
||||
foreach ($children as $childName => $childItems) {
|
||||
if (is_array($childItems) === true) {
|
||||
|
||||
// another tag with attributes
|
||||
if (A::isAssociative($childItems) === true) {
|
||||
$childTags[] = static::create($childItems, $childName, false, $level);
|
||||
|
||||
// just children
|
||||
} else {
|
||||
foreach ($childItems as $childItem) {
|
||||
$childTags[] = static::create($childItem, $childName, false, $nextLevel);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$childTags[] = static::tag($childName, $childItems, null, $indent);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($childTags) === false) {
|
||||
$value = $childTags;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $head === true ? '<?xml version="1.0" encoding="utf-8"?>' . PHP_EOL : null;
|
||||
$result .= static::tag($name, $value, $attributes, $indent);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all xml entities from a string
|
||||
* and convert them to html entities first
|
||||
* and remove all html entities afterwards.
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* echo xml::decode('some <em>über</em> crazy stuff');
|
||||
* // output: some über crazy stuff
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function decode(string $string = null): string
|
||||
{
|
||||
return Html::decode($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to a xml-safe string
|
||||
* Converts it to html-safe first and then it
|
||||
* will replace html entities to xml entities
|
||||
*
|
||||
* <code>
|
||||
*
|
||||
* echo xml::encode('some über crazy stuff');
|
||||
* // output: some über crazy stuff
|
||||
*
|
||||
* </code>
|
||||
*
|
||||
* @param string $string
|
||||
* @param boolean $html True: convert to html first
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(string $string = null, bool $html = true): string
|
||||
{
|
||||
if ($html === true) {
|
||||
$string = Html::encode($string, false);
|
||||
}
|
||||
|
||||
$entities = static::entities();
|
||||
$searches = array_keys($entities);
|
||||
$values = array_values($entities);
|
||||
|
||||
return str_replace($searches, $values, $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the html to xml entities translation table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function entities(): array
|
||||
{
|
||||
return static::$entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a XML string and returns an array
|
||||
*
|
||||
* @param string $xml
|
||||
* @return array|false
|
||||
*/
|
||||
public static function parse(string $xml = null)
|
||||
{
|
||||
$xml = preg_replace('/(<\/?)(\w+):([^>]*>)/', '$1$2$3', $xml);
|
||||
$xml = @simplexml_load_string($xml, null, LIBXML_NOENT | LIBXML_NOCDATA);
|
||||
|
||||
$xml = @json_encode($xml);
|
||||
$xml = @json_decode($xml, true);
|
||||
return is_array($xml) === true ? $xml : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an XML tag
|
||||
*
|
||||
* @param string $name
|
||||
* @param mixed $content
|
||||
* @param array $attr
|
||||
* @return string
|
||||
*/
|
||||
public static function tag(string $name, $content = null, array $attr = null, $indent = null): string
|
||||
{
|
||||
$attr = Html::attr($attr);
|
||||
$start = '<' . $name . ($attr ? ' ' . $attr : null) . '>';
|
||||
$end = '</' . $name . '>';
|
||||
|
||||
if (is_array($content) === true) {
|
||||
$xml = $indent . $start . PHP_EOL;
|
||||
foreach ($content as $line) {
|
||||
$xml .= $indent . $indent . $line . PHP_EOL;
|
||||
}
|
||||
$xml .= $indent . $end;
|
||||
} else {
|
||||
$xml = $indent . $start . static::value($content) . $end;
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the value as cdata if necessary
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
public static function value($value)
|
||||
{
|
||||
if ($value === true) {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
if ($value === false) {
|
||||
return 'false';
|
||||
}
|
||||
|
||||
if (is_numeric($value) === true) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Str::contains($value, '<![CDATA[') === true) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$encoded = htmlentities($value);
|
||||
|
||||
if ($encoded === $value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return '<![CDATA[' . static::encode($value) . ']]>';
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user