downgrade to kirby v3
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -30,9 +30,6 @@ class Collection extends Iterator implements Countable
|
||||
* Whether the collection keys should be
|
||||
* treated as case-sensitive
|
||||
*
|
||||
* @todo 5.0 Check if case-sensitive can become the
|
||||
* default mode, see https://github.com/getkirby/kirby/pull/5635
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $caseSensitive = false;
|
||||
@@ -70,7 +67,8 @@ class Collection extends Iterator implements Countable
|
||||
|
||||
/**
|
||||
* Improve var_dump() output
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
@@ -101,11 +99,11 @@ class Collection extends Iterator implements Countable
|
||||
*/
|
||||
public function __set(string $key, $value): void
|
||||
{
|
||||
if ($this->caseSensitive !== true) {
|
||||
$key = strtolower($key);
|
||||
if ($this->caseSensitive === true) {
|
||||
$this->data[$key] = $value;
|
||||
} else {
|
||||
$this->data[strtolower($key)] = $value;
|
||||
}
|
||||
|
||||
$this->data[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,10 +123,6 @@ class Collection extends Iterator implements Countable
|
||||
*/
|
||||
public function __unset($key)
|
||||
{
|
||||
if ($this->caseSensitive !== true) {
|
||||
$key = strtolower($key);
|
||||
}
|
||||
|
||||
unset($this->data[$key]);
|
||||
}
|
||||
|
||||
@@ -386,11 +380,11 @@ class Collection extends Iterator implements Countable
|
||||
public function find(...$keys)
|
||||
{
|
||||
if (count($keys) === 1) {
|
||||
if (is_array($keys[0]) === false) {
|
||||
if (is_array($keys[0]) === true) {
|
||||
$keys = $keys[0];
|
||||
} else {
|
||||
return $this->findByKey($keys[0]);
|
||||
}
|
||||
|
||||
$keys = $keys[0];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
@@ -523,24 +517,21 @@ class Collection extends Iterator implements Countable
|
||||
* Groups the elements by a given field or callback function
|
||||
*
|
||||
* @param string|Closure $field
|
||||
* @param bool $i
|
||||
* @return \Kirby\Toolkit\Collection A new collection with an element for
|
||||
* each group and a subcollection in
|
||||
* each group
|
||||
* @throws \Exception if $field is not a string nor a callback function
|
||||
*/
|
||||
public function group($field, bool $caseInsensitive = true)
|
||||
public function group($field, bool $i = true)
|
||||
{
|
||||
// group by field name
|
||||
if (is_string($field) === true) {
|
||||
return $this->group(function ($item) use ($field, $caseInsensitive) {
|
||||
return $this->group(function ($item) use ($field, $i) {
|
||||
$value = $this->getAttribute($item, $field);
|
||||
|
||||
// ignore upper/lowercase for group names
|
||||
if ($caseInsensitive === true) {
|
||||
return Str::lower($value);
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
return $i === true ? Str::lower($value) : $value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -560,14 +551,12 @@ class Collection extends Iterator implements Countable
|
||||
// make sure we have a proper key for each group
|
||||
if (is_array($value) === true) {
|
||||
throw new Exception('You cannot group by arrays or objects');
|
||||
}
|
||||
|
||||
if (is_object($value) === true) {
|
||||
} elseif (is_object($value) === true) {
|
||||
if (method_exists($value, '__toString') === false) {
|
||||
throw new Exception('You cannot group by arrays or objects');
|
||||
} else {
|
||||
$value = (string)$value;
|
||||
}
|
||||
|
||||
$value = (string)$value;
|
||||
}
|
||||
|
||||
if (isset($groups[$value]) === false) {
|
||||
@@ -745,17 +734,14 @@ class Collection extends Iterator implements Countable
|
||||
* Add pagination
|
||||
*
|
||||
* @param array ...$arguments
|
||||
* @return $this|static a sliced set of data
|
||||
* @return static a sliced set of data
|
||||
*/
|
||||
public function paginate(...$arguments)
|
||||
{
|
||||
$this->pagination = Pagination::for($this, ...$arguments);
|
||||
|
||||
// slice and clone the collection according to the pagination
|
||||
return $this->slice(
|
||||
$this->pagination->offset(),
|
||||
$this->pagination->limit()
|
||||
);
|
||||
return $this->slice($this->pagination->offset(), $this->pagination->limit());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1189,7 +1175,11 @@ class Collection extends Iterator implements Countable
|
||||
return $callback->call($this, $condition);
|
||||
}
|
||||
|
||||
return $fallback?->call($this, $condition) ?? $this;
|
||||
if ($fallback !== null) {
|
||||
return $fallback->call($this, $condition);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1258,58 +1248,74 @@ Collection::$filters['!='] = function ($collection, $field, $test, $split = fals
|
||||
* In Filter
|
||||
*/
|
||||
Collection::$filters['in'] = [
|
||||
'validator' => fn ($value, $test) => in_array($value, $test) === true,
|
||||
'strict' => false
|
||||
'validator' => function ($value, $test) {
|
||||
return in_array($value, $test) === true;
|
||||
},
|
||||
'strict' => false
|
||||
];
|
||||
|
||||
/**
|
||||
* Not In Filter
|
||||
*/
|
||||
Collection::$filters['not in'] = [
|
||||
'validator' => fn ($value, $test) => in_array($value, $test) === false
|
||||
'validator' => function ($value, $test) {
|
||||
return in_array($value, $test) === false;
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Contains Filter
|
||||
*/
|
||||
Collection::$filters['*='] = [
|
||||
'validator' => fn ($value, $test) => strpos($value, $test) !== false,
|
||||
'strict' => false
|
||||
'validator' => function ($value, $test) {
|
||||
return strpos($value, $test) !== false;
|
||||
},
|
||||
'strict' => false
|
||||
];
|
||||
|
||||
/**
|
||||
* Not Contains Filter
|
||||
*/
|
||||
Collection::$filters['!*='] = [
|
||||
'validator' => fn ($value, $test) => strpos($value, $test) === false
|
||||
'validator' => function ($value, $test) {
|
||||
return strpos($value, $test) === false;
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* More Filter
|
||||
*/
|
||||
Collection::$filters['>'] = [
|
||||
'validator' => fn ($value, $test) => $value > $test
|
||||
'validator' => function ($value, $test) {
|
||||
return $value > $test;
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Min Filter
|
||||
*/
|
||||
Collection::$filters['>='] = [
|
||||
'validator' => fn ($value, $test) => $value >= $test
|
||||
'validator' => function ($value, $test) {
|
||||
return $value >= $test;
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Less Filter
|
||||
*/
|
||||
Collection::$filters['<'] = [
|
||||
'validator' => fn ($value, $test) => $value < $test
|
||||
'validator' => function ($value, $test) {
|
||||
return $value < $test;
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Max Filter
|
||||
*/
|
||||
Collection::$filters['<='] = [
|
||||
'validator' => fn ($value, $test) => $value <= $test
|
||||
'validator' => function ($value, $test) {
|
||||
return $value <= $test;
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -1324,7 +1330,9 @@ Collection::$filters['$='] = [
|
||||
* Not Ends With Filter
|
||||
*/
|
||||
Collection::$filters['!$='] = [
|
||||
'validator' => fn ($value, $test) => V::endsWith($value, $test) === false
|
||||
'validator' => function ($value, $test) {
|
||||
return V::endsWith($value, $test) === false;
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -1339,15 +1347,19 @@ Collection::$filters['^='] = [
|
||||
* Not Starts With Filter
|
||||
*/
|
||||
Collection::$filters['!^='] = [
|
||||
'validator' => fn ($value, $test) => V::startsWith($value, $test) === false
|
||||
'validator' => function ($value, $test) {
|
||||
return V::startsWith($value, $test) === false;
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Between Filter
|
||||
*/
|
||||
Collection::$filters['between'] = Collection::$filters['..'] = [
|
||||
'validator' => fn ($value, $test) => V::between($value, ...$test) === true,
|
||||
'strict' => false
|
||||
'validator' => function ($value, $test) {
|
||||
return V::between($value, ...$test) === true;
|
||||
},
|
||||
'strict' => false
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -1362,7 +1374,9 @@ Collection::$filters['*'] = [
|
||||
* Not Match Filter
|
||||
*/
|
||||
Collection::$filters['!*'] = [
|
||||
'validator' => fn ($value, $test) => V::match($value, $test) === false
|
||||
'validator' => function ($value, $test) {
|
||||
return V::match($value, $test) === false;
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -1397,49 +1411,62 @@ Collection::$filters['minwords'] = [
|
||||
* Date Equals Filter
|
||||
*/
|
||||
Collection::$filters['date =='] = [
|
||||
'validator' => fn ($value, $test) => V::date($value, '==', $test)
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '==', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Not Equals Filter
|
||||
*/
|
||||
Collection::$filters['date !='] = [
|
||||
'validator' => fn ($value, $test) => V::date($value, '!=', $test)
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '!=', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date More Filter
|
||||
*/
|
||||
Collection::$filters['date >'] = [
|
||||
'validator' => fn ($value, $test) => V::date($value, '>', $test)
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '>', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Min Filter
|
||||
*/
|
||||
Collection::$filters['date >='] = [
|
||||
'validator' => fn ($value, $test) => V::date($value, '>=', $test)
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '>=', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Less Filter
|
||||
*/
|
||||
Collection::$filters['date <'] = [
|
||||
'validator' => fn ($value, $test) => V::date($value, '<', $test)
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '<', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Max Filter
|
||||
*/
|
||||
Collection::$filters['date <='] = [
|
||||
'validator' => fn ($value, $test) => V::date($value, '<=', $test)
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '<=', $test);
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Date Between Filter
|
||||
*/
|
||||
Collection::$filters['date between'] = Collection::$filters['date ..'] = [
|
||||
'validator' => fn ($value, $test) =>
|
||||
V::date($value, '>=', $test[0]) &&
|
||||
V::date($value, '<=', $test[1])
|
||||
'validator' => function ($value, $test) {
|
||||
return V::date($value, '>=', $test[0]) &&
|
||||
V::date($value, '<=', $test[1]);
|
||||
}
|
||||
];
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use AllowDynamicProperties;
|
||||
use ArgumentCountError;
|
||||
use Closure;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Filesystem\F;
|
||||
@@ -18,56 +16,95 @@ use TypeError;
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* @todo remove the following psalm suppress when PHP >= 8.2 required
|
||||
* @psalm-suppress UndefinedAttributeClass
|
||||
*/
|
||||
#[AllowDynamicProperties]
|
||||
class Component
|
||||
{
|
||||
/**
|
||||
* Registry for all component mixins
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static array $mixins = [];
|
||||
public static $mixins = [];
|
||||
|
||||
/**
|
||||
* Registry for all component types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static array $types = [];
|
||||
public static $types = [];
|
||||
|
||||
/**
|
||||
* An array of all passed attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $attrs = [];
|
||||
protected $attrs = [];
|
||||
|
||||
/**
|
||||
* An array of all computed properties
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $computed = [];
|
||||
protected $computed = [];
|
||||
|
||||
/**
|
||||
* An array of all registered methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $methods = [];
|
||||
protected $methods = [];
|
||||
|
||||
/**
|
||||
* An array of all component options
|
||||
* from the component definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array|string $options = [];
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* An array of all resolved props
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $props = [];
|
||||
protected $props = [];
|
||||
|
||||
/**
|
||||
* The component type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $type;
|
||||
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 = [])
|
||||
{
|
||||
@@ -76,7 +113,7 @@ class Component
|
||||
}
|
||||
|
||||
$this->attrs = $attrs;
|
||||
$this->options = $options = static::setup($type);
|
||||
$this->options = $options = $this->setup($type);
|
||||
$this->methods = $methods = $options['methods'] ?? [];
|
||||
|
||||
foreach ($attrs as $attrName => $attrValue) {
|
||||
@@ -97,29 +134,10 @@ class Component
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic caller for defined methods and properties
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
@@ -129,6 +147,9 @@ class Component
|
||||
/**
|
||||
* Fallback for missing properties to return
|
||||
* null instead of an error
|
||||
*
|
||||
* @param string $attr
|
||||
* @return null
|
||||
*/
|
||||
public function __get(string $attr)
|
||||
{
|
||||
@@ -140,6 +161,8 @@ class Component
|
||||
* This can be overwritten by extended classes
|
||||
* to define basic options that should always
|
||||
* be applied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function defaults(): array
|
||||
{
|
||||
@@ -149,50 +172,54 @@ class Component
|
||||
/**
|
||||
* Register all defined props and apply the
|
||||
* passed values.
|
||||
*
|
||||
* @param array $props
|
||||
* @return void
|
||||
*/
|
||||
protected function applyProps(array $props): void
|
||||
{
|
||||
foreach ($props as $name => $function) {
|
||||
if ($function instanceof Closure) {
|
||||
if (isset($this->attrs[$name]) === true) {
|
||||
foreach ($props as $propName => $propFunction) {
|
||||
if (is_a($propFunction, 'Closure') === true) {
|
||||
if (isset($this->attrs[$propName]) === true) {
|
||||
try {
|
||||
$this->$name = $this->props[$name] = $function->call(
|
||||
$this,
|
||||
$this->attrs[$name]
|
||||
);
|
||||
continue;
|
||||
} catch (TypeError) {
|
||||
throw new TypeError('Invalid value for "' . $name . '"');
|
||||
$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 . '"');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->$name = $this->props[$name] = $function->call($this);
|
||||
continue;
|
||||
} catch (ArgumentCountError) {
|
||||
throw new ArgumentCountError('Please provide a value for "' . $name . '"');
|
||||
}
|
||||
} else {
|
||||
$this->$propName = $this->props[$propName] = $propFunction;
|
||||
}
|
||||
|
||||
$this->$name = $this->props[$name] = $function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $name => $function) {
|
||||
if ($function instanceof Closure) {
|
||||
$this->$name = $this->computed[$name] = $function->call($this);
|
||||
foreach ($computed as $computedName => $computedFunction) {
|
||||
if (is_a($computedFunction, 'Closure') === true) {
|
||||
$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
|
||||
{
|
||||
@@ -204,10 +231,7 @@ class Component
|
||||
throw new Exception('Component definition ' . $definition . ' does not exist');
|
||||
}
|
||||
|
||||
static::$types[$type] = $definition = F::load(
|
||||
$definition,
|
||||
allowOutput: false
|
||||
);
|
||||
static::$types[$type] = $definition = F::load($definition);
|
||||
}
|
||||
|
||||
return $definition;
|
||||
@@ -218,6 +242,9 @@ class Component
|
||||
* 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
|
||||
{
|
||||
@@ -226,32 +253,23 @@ class Component
|
||||
|
||||
if (isset($definition['extends']) === true) {
|
||||
// extend other definitions
|
||||
$options = array_replace_recursive(
|
||||
static::defaults(),
|
||||
static::load($definition['extends']),
|
||||
$definition
|
||||
);
|
||||
$options = array_replace_recursive(static::defaults(), static::load($definition['extends']), $definition);
|
||||
} else {
|
||||
// inject defaults
|
||||
$options = array_replace_recursive(static::defaults(), $definition);
|
||||
}
|
||||
|
||||
// inject mixins
|
||||
foreach ($options['mixins'] ?? [] as $mixin) {
|
||||
if (isset(static::$mixins[$mixin]) === true) {
|
||||
if (is_string(static::$mixins[$mixin]) === true) {
|
||||
// resolve a path to a mixin on demand
|
||||
if (isset($options['mixins']) === true) {
|
||||
foreach ($options['mixins'] as $mixin) {
|
||||
if (isset(static::$mixins[$mixin]) === true) {
|
||||
if (is_string(static::$mixins[$mixin]) === true) {
|
||||
// resolve a path to a mixin on demand
|
||||
static::$mixins[$mixin] = include static::$mixins[$mixin];
|
||||
}
|
||||
|
||||
static::$mixins[$mixin] = F::load(
|
||||
static::$mixins[$mixin],
|
||||
allowOutput: false
|
||||
);
|
||||
$options = array_replace_recursive(static::$mixins[$mixin], $options);
|
||||
}
|
||||
|
||||
$options = array_replace_recursive(
|
||||
static::$mixins[$mixin],
|
||||
$options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,13 +278,13 @@ class Component
|
||||
|
||||
/**
|
||||
* Converts all props and computed props to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$closure = $this->options['toArray'] ?? null;
|
||||
|
||||
if ($closure instanceof Closure) {
|
||||
return $closure->call($this);
|
||||
if (is_a($this->options['toArray'] ?? null, 'Closure') === true) {
|
||||
return $this->options['toArray']->call($this);
|
||||
}
|
||||
|
||||
$array = array_merge($this->attrs, $this->props, $this->computed);
|
||||
|
||||
@@ -14,5 +14,8 @@ namespace Kirby\Toolkit;
|
||||
*/
|
||||
class Config extends Silo
|
||||
{
|
||||
public static array $data = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $data = [];
|
||||
}
|
||||
|
||||
@@ -19,8 +19,11 @@ use ReflectionFunction;
|
||||
*/
|
||||
class Controller
|
||||
{
|
||||
public function __construct(protected Closure $function)
|
||||
protected $function;
|
||||
|
||||
public function __construct(Closure $function)
|
||||
{
|
||||
$this->function = $function;
|
||||
}
|
||||
|
||||
public function arguments(array $data = []): array
|
||||
@@ -28,20 +31,9 @@ class Controller
|
||||
$info = new ReflectionFunction($this->function);
|
||||
$args = [];
|
||||
|
||||
foreach ($info->getParameters() as $param) {
|
||||
$name = $param->getName();
|
||||
|
||||
if ($param->isVariadic() === true) {
|
||||
// variadic ... argument collects all remaining values
|
||||
$args += $data;
|
||||
} elseif (isset($data[$name]) === true) {
|
||||
// use provided argument value if available
|
||||
$args[$name] = $data[$name];
|
||||
} elseif ($param->isDefaultValueAvailable() === false) {
|
||||
// use null for any other arguments that don't define
|
||||
// a default value for themselves
|
||||
$args[$name] = null;
|
||||
}
|
||||
foreach ($info->getParameters() as $parameter) {
|
||||
$name = $parameter->getName();
|
||||
$args[] = $data[$name] ?? null;
|
||||
}
|
||||
|
||||
return $args;
|
||||
@@ -49,18 +41,16 @@ class Controller
|
||||
|
||||
public function call($bind = null, $data = [])
|
||||
{
|
||||
// unwrap lazy values in arguments
|
||||
$args = $this->arguments($data);
|
||||
$args = LazyValue::unwrap($args);
|
||||
|
||||
if ($bind === null) {
|
||||
return ($this->function)(...$args);
|
||||
return call_user_func($this->function, ...$args);
|
||||
}
|
||||
|
||||
return $this->function->call($bind, ...$args);
|
||||
}
|
||||
|
||||
public static function load(string $file): static|null
|
||||
public static function load(string $file)
|
||||
{
|
||||
if (is_file($file) === false) {
|
||||
return null;
|
||||
@@ -68,7 +58,7 @@ class Controller
|
||||
|
||||
$function = F::load($file);
|
||||
|
||||
if ($function instanceof Closure === false) {
|
||||
if (is_a($function, 'Closure') === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use IntlDateFormatter;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
@@ -29,15 +26,13 @@ class Date extends DateTime
|
||||
* @param string|int|\DateTimeInterface $datetime Datetime string, UNIX timestamp or object
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
*/
|
||||
public function __construct(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
) {
|
||||
public function __construct($datetime = 'now', ?DateTimeZone $timezone = null)
|
||||
{
|
||||
if (is_int($datetime) === true) {
|
||||
$datetime = date('r', $datetime);
|
||||
}
|
||||
|
||||
if ($datetime instanceof DateTimeInterface) {
|
||||
if (is_a($datetime, 'DateTimeInterface') === true) {
|
||||
$datetime = $datetime->format('r');
|
||||
}
|
||||
|
||||
@@ -46,6 +41,8 @@ class Date extends DateTime
|
||||
|
||||
/**
|
||||
* Returns the datetime in `YYYY-MM-DD hh:mm:ss` format with timezone
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
@@ -60,7 +57,7 @@ class Date extends DateTime
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the unit name is invalid
|
||||
*/
|
||||
public function ceil(string $unit): static
|
||||
public function ceil(string $unit)
|
||||
{
|
||||
static::validateUnit($unit);
|
||||
|
||||
@@ -72,19 +69,22 @@ class Date extends DateTime
|
||||
/**
|
||||
* Returns the interval between the provided and the object's datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return \DateInterval
|
||||
*/
|
||||
public function compare(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): DateInterval {
|
||||
public function compare($datetime = 'now', ?DateTimeZone $timezone = null)
|
||||
{
|
||||
return $this->diff(new static($datetime, $timezone));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the day value
|
||||
*
|
||||
* @param int|null $day
|
||||
* @return int
|
||||
*/
|
||||
public function day(int|null $day = null): int
|
||||
public function day(?int $day = null): int
|
||||
{
|
||||
if ($day === null) {
|
||||
return (int)$this->format('d');
|
||||
@@ -102,42 +102,31 @@ class Date extends DateTime
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the unit name is invalid
|
||||
*/
|
||||
public function floor(string $unit): static
|
||||
public function floor(string $unit)
|
||||
{
|
||||
static::validateUnit($unit);
|
||||
|
||||
$formats = [
|
||||
'year' => 'Y-01-01',
|
||||
'month' => 'Y-m-01',
|
||||
'day' => 'Y-m-d',
|
||||
'hour' => 'Y-m-d H:00:00',
|
||||
'minute' => 'Y-m-d H:i:00',
|
||||
'second' => 'Y-m-d H:i:s'
|
||||
'year' => 'Y-01-01P',
|
||||
'month' => 'Y-m-01P',
|
||||
'day' => 'Y-m-dP',
|
||||
'hour' => 'Y-m-d H:00:00P',
|
||||
'minute' => 'Y-m-d H:i:00P',
|
||||
'second' => 'Y-m-d H:i:sP'
|
||||
];
|
||||
|
||||
$flooredDate = $this->format($formats[$unit]);
|
||||
$this->set($flooredDate, $this->timezone());
|
||||
$flooredDate = date($formats[$unit], $this->timestamp());
|
||||
$this->set($flooredDate);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the datetime value with a custom handler
|
||||
* or with the globally configured one
|
||||
*
|
||||
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
|
||||
* for the globally configured one
|
||||
*/
|
||||
public function formatWithHandler(
|
||||
string|IntlDateFormatter|null $format = null,
|
||||
string|null $handler = null
|
||||
): string|int|false {
|
||||
return Str::date($this->timestamp(), $format, $handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the hour value
|
||||
*
|
||||
* @param int|null $hour
|
||||
* @return int
|
||||
*/
|
||||
public function hour(int|null $hour = null): int
|
||||
public function hour(?int $hour = null): int
|
||||
{
|
||||
if ($hour === null) {
|
||||
return (int)$this->format('H');
|
||||
@@ -150,93 +139,102 @@ class Date extends DateTime
|
||||
/**
|
||||
* Checks if the object's datetime is the same as the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function is(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
public function is($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
return $this == new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is after the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function isAfter(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
public function isAfter($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
return $this > new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is before the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function isBefore(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
public function isBefore($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
return $this < new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is between the given datetimes
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $min
|
||||
* @param string|int|\DateTimeInterface $max
|
||||
* @return bool
|
||||
*/
|
||||
public function isBetween(
|
||||
string|int|DateTimeInterface $min,
|
||||
string|int|DateTimeInterface $max
|
||||
): bool {
|
||||
public function isBetween($min, $max): bool
|
||||
{
|
||||
return $this->isMin($min) === true && $this->isMax($max) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is at or before the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function isMax(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
public function isMax($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
return $this <= new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the object's datetime is at or after the given datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface $datetime
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
* @return bool
|
||||
*/
|
||||
public function isMin(
|
||||
string|int|DateTimeInterface $datetime = 'now',
|
||||
DateTimeZone|null $timezone = null
|
||||
): bool {
|
||||
public function isMin($datetime = 'now', ?DateTimeZone $timezone = null): bool
|
||||
{
|
||||
return $this >= new static($datetime, $timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the microsecond value
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function microsecond(): int
|
||||
{
|
||||
return (int)$this->format('u');
|
||||
return $this->format('u');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the millisecond value
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function millisecond(): int
|
||||
{
|
||||
return (int)$this->format('v');
|
||||
return $this->format('v');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the minute value
|
||||
*
|
||||
* @param int|null $minute
|
||||
* @return int
|
||||
*/
|
||||
public function minute(int|null $minute = null): int
|
||||
public function minute(?int $minute = null): int
|
||||
{
|
||||
if ($minute === null) {
|
||||
return (int)$this->format('i');
|
||||
@@ -248,8 +246,11 @@ class Date extends DateTime
|
||||
|
||||
/**
|
||||
* Gets or sets the month value
|
||||
*
|
||||
* @param int|null $month
|
||||
* @return int
|
||||
*/
|
||||
public function month(int|null $month = null): int
|
||||
public function month(?int $month = null): int
|
||||
{
|
||||
if ($month === null) {
|
||||
return (int)$this->format('m');
|
||||
@@ -263,10 +264,10 @@ class Date extends DateTime
|
||||
* Returns the datetime which is nearest to the object's datetime
|
||||
*
|
||||
* @param string|int|\DateTimeInterface ...$datetime Datetime strings, UNIX timestamps or objects
|
||||
* @return string|int|\DateTimeInterface
|
||||
*/
|
||||
public function nearest(
|
||||
string|int|DateTimeInterface ...$datetime
|
||||
): string|int|DateTimeInterface {
|
||||
public function nearest(...$datetime)
|
||||
{
|
||||
$timestamp = $this->timestamp();
|
||||
$minDiff = PHP_INT_MAX;
|
||||
$nearest = null;
|
||||
@@ -289,8 +290,9 @@ class Date extends DateTime
|
||||
* Returns an instance of the current datetime
|
||||
*
|
||||
* @param \DateTimeZone|null $timezone
|
||||
* @return static
|
||||
*/
|
||||
public static function now(DateTimeZone|null $timezone = null): static
|
||||
public static function now(?DateTimeZone $timezone = null)
|
||||
{
|
||||
return new static('now', $timezone);
|
||||
}
|
||||
@@ -298,18 +300,20 @@ class Date extends DateTime
|
||||
/**
|
||||
* Tries to create an instance from the given string
|
||||
* or fails silently by returning `null` on error
|
||||
*
|
||||
* @param string|null $datetime
|
||||
* @param \DateTimeZone|null $timezone
|
||||
* @return static|null
|
||||
*/
|
||||
public static function optional(
|
||||
string|null $datetime = null,
|
||||
DateTimeZone|null $timezone = null
|
||||
): static|null {
|
||||
public static function optional(?string $datetime = null, ?DateTimeZone $timezone = null)
|
||||
{
|
||||
if (empty($datetime) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new static($datetime, $timezone);
|
||||
} catch (Exception) {
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -323,7 +327,7 @@ class Date extends DateTime
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the unit name or size is invalid
|
||||
*/
|
||||
public function round(string $unit, int $size = 1): static
|
||||
public function round(string $unit, int $size = 1)
|
||||
{
|
||||
static::validateUnit($unit);
|
||||
|
||||
@@ -360,12 +364,12 @@ class Date extends DateTime
|
||||
* by the defined step
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param string|null $date
|
||||
* @param int|array|null $step array of `unit` and `size` to round to nearest
|
||||
* @return int|null
|
||||
*/
|
||||
public static function roundedTimestamp(
|
||||
string|null $date = null,
|
||||
int|array|null $step = null
|
||||
): int|null {
|
||||
public static function roundedTimestamp(?string $date = null, $step = null): ?int
|
||||
{
|
||||
if ($date = static::optional($date)) {
|
||||
if ($step !== null) {
|
||||
$step = static::stepConfig($step, [
|
||||
@@ -383,8 +387,11 @@ class Date extends DateTime
|
||||
|
||||
/**
|
||||
* Gets or sets the second value
|
||||
*
|
||||
* @param int|null $second
|
||||
* @return int
|
||||
*/
|
||||
public function second(int|null $second = null): int
|
||||
public function second(?int $second = null): int
|
||||
{
|
||||
if ($second === null) {
|
||||
return (int)$this->format('s');
|
||||
@@ -400,10 +407,8 @@ class Date extends DateTime
|
||||
* @param string|int|\DateTimeInterface $datetime Datetime string, UNIX timestamp or object
|
||||
* @param \DateTimeZone|null $timezone Optional default timezone if `$datetime` is string
|
||||
*/
|
||||
public function set(
|
||||
string|int|DateTimeInterface $datetime,
|
||||
DateTimeZone|null $timezone = null
|
||||
): void {
|
||||
public function set($datetime, ?DateTimeZone $timezone = null)
|
||||
{
|
||||
$datetime = new static($datetime, $timezone);
|
||||
$this->setTimestamp($datetime->timestamp());
|
||||
}
|
||||
@@ -414,12 +419,15 @@ class Date extends DateTime
|
||||
* @param array|string|int|null $input Full array with `size` and/or `unit` keys, `unit`
|
||||
* string, `size` int or `null` for the default
|
||||
* @param array|null $default Default values to use if one or both values are not provided
|
||||
* @return array
|
||||
*/
|
||||
public static function stepConfig(
|
||||
// no type hint to use InvalidArgumentException at the end
|
||||
$input = null,
|
||||
array|null $default = ['size' => 1, 'unit' => 'day']
|
||||
): array {
|
||||
public static function stepConfig($input = null, ?array $default = null): array
|
||||
{
|
||||
$default ??= [
|
||||
'size' => 1,
|
||||
'unit' => 'day'
|
||||
];
|
||||
|
||||
if ($input === null) {
|
||||
return $default;
|
||||
}
|
||||
@@ -443,6 +451,8 @@ class Date extends DateTime
|
||||
|
||||
/**
|
||||
* Returns the time in `hh:mm:ss` format
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function time(): string
|
||||
{
|
||||
@@ -451,6 +461,8 @@ class Date extends DateTime
|
||||
|
||||
/**
|
||||
* Returns the UNIX timestamp
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function timestamp(): int
|
||||
{
|
||||
@@ -459,16 +471,21 @@ class Date extends DateTime
|
||||
|
||||
/**
|
||||
* Returns the timezone object
|
||||
*
|
||||
* @return \DateTimeZone
|
||||
*/
|
||||
public function timezone(): DateTimeZone|false
|
||||
public function timezone()
|
||||
{
|
||||
return $this->getTimezone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of the beginning of the current day
|
||||
*
|
||||
* @param \DateTimeZone|null $timezone
|
||||
* @return static
|
||||
*/
|
||||
public static function today(DateTimeZone|null $timezone = null): static
|
||||
public static function today(?DateTimeZone $timezone = null)
|
||||
{
|
||||
return new static('today', $timezone);
|
||||
}
|
||||
@@ -479,19 +496,25 @@ class Date extends DateTime
|
||||
*
|
||||
* @param string $mode `date`, `time` or `datetime`
|
||||
* @param bool $timezone Whether the timezone is printed as well
|
||||
* @return string
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the mode is invalid
|
||||
*/
|
||||
public function toString(
|
||||
string $mode = 'datetime',
|
||||
bool $timezone = true
|
||||
): string {
|
||||
$format = match ($mode) {
|
||||
'date' => 'Y-m-d',
|
||||
'time' => 'H:i:s',
|
||||
'datetime' => 'Y-m-d H:i:s',
|
||||
default => throw new InvalidArgumentException('Invalid mode')
|
||||
};
|
||||
public function toString(string $mode = 'datetime', bool $timezone = true): string
|
||||
{
|
||||
switch ($mode) {
|
||||
case 'date':
|
||||
$format = 'Y-m-d';
|
||||
break;
|
||||
case 'time':
|
||||
$format = 'H:i:s';
|
||||
break;
|
||||
case 'datetime':
|
||||
$format = 'Y-m-d H:i:s';
|
||||
break;
|
||||
default:
|
||||
throw new InvalidArgumentException('Invalid mode');
|
||||
}
|
||||
|
||||
if ($timezone === true) {
|
||||
$format .= 'P';
|
||||
@@ -502,8 +525,11 @@ class Date extends DateTime
|
||||
|
||||
/**
|
||||
* Gets or sets the year value
|
||||
*
|
||||
* @param int|null $year
|
||||
* @return int
|
||||
*/
|
||||
public function year(int|null $year = null): int
|
||||
public function year(?int $year = null): int
|
||||
{
|
||||
if ($year === null) {
|
||||
return (int)$this->format('Y');
|
||||
@@ -516,6 +542,9 @@ class Date extends DateTime
|
||||
/**
|
||||
* Ensures that the provided string is a valid unit name
|
||||
*
|
||||
* @param string $unit
|
||||
* @return void
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException
|
||||
*/
|
||||
protected static function validateUnit(string $unit): void
|
||||
|
||||
@@ -8,11 +8,10 @@ use DOMDocument;
|
||||
use DOMDocumentType;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use DOMNodeList;
|
||||
use DOMProcessingInstruction;
|
||||
use DOMText;
|
||||
use DOMXPath;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
@@ -69,6 +68,12 @@ class Dom
|
||||
$this->doc = new DOMDocument();
|
||||
|
||||
$loaderSetting = null;
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
// prevent loading external entities to protect against XXE attacks;
|
||||
// only needed for PHP versions before 8.0 (the function was deprecated
|
||||
// as the disabled state is the new default in PHP 8.0+)
|
||||
$loaderSetting = libxml_disable_entity_loader(true);
|
||||
}
|
||||
|
||||
// switch to "user error handling"
|
||||
$intErrorsSetting = libxml_use_internal_errors(true);
|
||||
@@ -101,6 +106,12 @@ class Dom
|
||||
$load = $this->doc->loadXML($code);
|
||||
}
|
||||
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
// ensure that we don't alter global state by
|
||||
// resetting the original value
|
||||
libxml_disable_entity_loader($loaderSetting);
|
||||
}
|
||||
|
||||
// get one error for use below and reset the global state
|
||||
$error = libxml_get_last_error();
|
||||
libxml_clear_errors();
|
||||
@@ -122,16 +133,20 @@ class Dom
|
||||
|
||||
/**
|
||||
* Returns the HTML body if one exists
|
||||
*
|
||||
* @return \DOMElement|null
|
||||
*/
|
||||
public function body(): DOMElement|null
|
||||
public function body()
|
||||
{
|
||||
return $this->body ??= $this->query('/html/body')[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the document object
|
||||
*
|
||||
* @return \DOMDocument
|
||||
*/
|
||||
public function document(): DOMDocument
|
||||
public function document()
|
||||
{
|
||||
return $this->doc;
|
||||
}
|
||||
@@ -139,6 +154,9 @@ class Dom
|
||||
/**
|
||||
* Extracts all URLs wrapped in a url() wrapper. E.g. for style attributes.
|
||||
* @internal
|
||||
*
|
||||
* @param string $value
|
||||
* @return array
|
||||
*/
|
||||
public static function extractUrls(string $value): array
|
||||
{
|
||||
@@ -163,14 +181,12 @@ class Dom
|
||||
* Checks for allowed attributes according to the allowlist
|
||||
* @internal
|
||||
*
|
||||
* @param \DOMAttr $attr
|
||||
* @param array $options
|
||||
* @return true|string If not allowed, an error message is returned
|
||||
*/
|
||||
public static function isAllowedAttr(
|
||||
DOMAttr $attr,
|
||||
array $options
|
||||
): bool|string {
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
public static function isAllowedAttr(DOMAttr $attr, array $options)
|
||||
{
|
||||
$allowedTags = $options['allowedTags'];
|
||||
|
||||
// check if the attribute is in the list of global allowed attributes
|
||||
@@ -213,14 +229,12 @@ class Dom
|
||||
* Checks for allowed attributes according to the global allowlist
|
||||
* @internal
|
||||
*
|
||||
* @param \DOMAttr $attr
|
||||
* @param array $options
|
||||
* @return true|string If not allowed, an error message is returned
|
||||
*/
|
||||
public static function isAllowedGlobalAttr(
|
||||
DOMAttr $attr,
|
||||
array $options
|
||||
): bool|string {
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
public static function isAllowedGlobalAttr(DOMAttr $attr, array $options)
|
||||
{
|
||||
$allowedAttrs = $options['allowedAttrs'];
|
||||
|
||||
if ($allowedAttrs === true) {
|
||||
@@ -253,14 +267,12 @@ class Dom
|
||||
* Checks if the URL is acceptable for URL attributes
|
||||
* @internal
|
||||
*
|
||||
* @param string $url
|
||||
* @param array $options
|
||||
* @return true|string If not allowed, an error message is returned
|
||||
*/
|
||||
public static function isAllowedUrl(
|
||||
string $url,
|
||||
array $options
|
||||
): bool|string {
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
public static function isAllowedUrl(string $url, array $options)
|
||||
{
|
||||
$url = Str::lower($url);
|
||||
|
||||
// allow empty URL values
|
||||
@@ -280,7 +292,7 @@ class Dom
|
||||
|
||||
// allow site-internal URLs that didn't match the
|
||||
// protocol-relative check above
|
||||
if (mb_substr($url, 0, 1) === '/' && $options['allowHostRelativeUrls'] !== true) {
|
||||
if (mb_substr($url, 0, 1) === '/') {
|
||||
// if a CMS instance is active, only allow the URL
|
||||
// if it doesn't point outside of the index URL
|
||||
if ($kirby = App::instance(null, true)) {
|
||||
@@ -392,6 +404,8 @@ class Dom
|
||||
* Otherwise DOMDocument won't be available and the Dom cannot
|
||||
* work at all.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function isSupported(): bool
|
||||
@@ -401,6 +415,9 @@ class Dom
|
||||
|
||||
/**
|
||||
* Returns the XML or HTML markup contained in the node
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return string
|
||||
*/
|
||||
public function innerMarkup(DOMNode $node): string
|
||||
{
|
||||
@@ -419,21 +436,20 @@ class Dom
|
||||
* the allowed namespaces
|
||||
* @internal
|
||||
*
|
||||
* @param array $list
|
||||
* @param \DOMNode $node
|
||||
* @param array $options See `Dom::sanitize()`
|
||||
* @param \Closure|null Comparison callback that returns whether the expected and real name match
|
||||
* @return string|false Matched name in the list or `false`
|
||||
*/
|
||||
public static function listContainsName(
|
||||
array $list,
|
||||
DOMNode $node,
|
||||
array $options,
|
||||
Closure|null $compare = null
|
||||
): string|false {
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
public static function listContainsName(array $list, DOMNode $node, array $options, ?Closure $compare = null)
|
||||
{
|
||||
$allowedNamespaces = $options['allowedNamespaces'];
|
||||
$localName = $node->localName;
|
||||
$compare ??= fn ($expected, $real): bool => $expected === $real;
|
||||
|
||||
if ($compare === null) {
|
||||
$compare = fn ($expected, $real): bool => $expected === $real;
|
||||
}
|
||||
|
||||
// if the configuration does not define namespace URIs or if the
|
||||
// currently checked node is from the special `xml:` namespace
|
||||
@@ -464,7 +480,7 @@ class Dom
|
||||
$namespaceUri = null;
|
||||
$itemLocal = $item;
|
||||
if (Str::contains($item, ':') === true) {
|
||||
[$namespaceName, $itemLocal] = explode(':', $item);
|
||||
list($namespaceName, $itemLocal) = explode(':', $item);
|
||||
$namespaceUri = $allowedNamespaces[$namespaceName] ?? null;
|
||||
} else {
|
||||
// list items without namespace are from the default namespace
|
||||
@@ -472,19 +488,13 @@ class Dom
|
||||
}
|
||||
|
||||
// try if we can find an exact namespaced match
|
||||
if (
|
||||
$namespaceUri === $node->namespaceURI &&
|
||||
$compare($itemLocal, $localName) === true
|
||||
) {
|
||||
if ($namespaceUri === $node->namespaceURI && $compare($itemLocal, $localName) === true) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
// also try to match the fully-qualified name
|
||||
// if the document doesn't define the namespace
|
||||
if (
|
||||
$node->namespaceURI === null &&
|
||||
$compare($item, $node->nodeName) === true
|
||||
) {
|
||||
if ($node->namespaceURI === null && $compare($item, $node->nodeName) === true) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
@@ -494,6 +504,9 @@ class Dom
|
||||
|
||||
/**
|
||||
* Removes a node from the document
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return void
|
||||
*/
|
||||
public static function remove(DOMNode $node): void
|
||||
{
|
||||
@@ -503,12 +516,12 @@ class Dom
|
||||
/**
|
||||
* Executes an XPath query in the document
|
||||
*
|
||||
* @param string $query
|
||||
* @param \DOMNode|null $node Optional context node for relative queries
|
||||
* @return \DOMNodeList|false
|
||||
*/
|
||||
public function query(
|
||||
string $query,
|
||||
DOMNode|null $node = null
|
||||
): DOMNodeList|false {
|
||||
public function query(string $query, ?DOMNode $node = null)
|
||||
{
|
||||
return (new DOMXPath($this->doc))->query($query, $node);
|
||||
}
|
||||
|
||||
@@ -525,9 +538,6 @@ class Dom
|
||||
* or `true` for any
|
||||
* - `allowedDomains`: Allowed hostnames for HTTP(S) URLs in `urlAttrs`
|
||||
* and inside `url()` wrappers or `true` for any
|
||||
* - `allowHostRelativeUrls`: Whether URLs that begin with `/` should be
|
||||
* allowed even if the site index URL is in a subfolder (useful when using
|
||||
* the HTML `<base>` element where the sanitized code will be rendered)
|
||||
* - `allowedNamespaces`: Associative array of all allowed namespace URIs;
|
||||
* the array keys are reference names that can be referred to from the
|
||||
* `allowedAttrPrefixes`, `allowedAttrs`, `allowedTags`, `disallowedTags`
|
||||
@@ -558,7 +568,20 @@ class Dom
|
||||
*/
|
||||
public function sanitize(array $options): array
|
||||
{
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
$options = array_merge([
|
||||
'allowedAttrPrefixes' => [],
|
||||
'allowedAttrs' => true,
|
||||
'allowedDataUris' => true,
|
||||
'allowedDomains' => true,
|
||||
'allowedNamespaces' => true,
|
||||
'allowedPIs' => true,
|
||||
'allowedTags' => true,
|
||||
'attrCallback' => null,
|
||||
'disallowedTags' => [],
|
||||
'doctypeCallback' => null,
|
||||
'elementCallback' => null,
|
||||
'urlAttrs' => ['href', 'src', 'xlink:href'],
|
||||
], $options);
|
||||
|
||||
$errors = [];
|
||||
|
||||
@@ -566,7 +589,7 @@ class Dom
|
||||
// convert the `DOMNodeList` to an array first, otherwise removing
|
||||
// nodes would shift the list and make subsequent operations fail
|
||||
foreach (iterator_to_array($this->doc->childNodes, false) as $child) {
|
||||
if ($child instanceof DOMDocumentType) {
|
||||
if (is_a($child, 'DOMDocumentType') === true) {
|
||||
$this->sanitizeDoctype($child, $options, $errors);
|
||||
}
|
||||
}
|
||||
@@ -593,6 +616,7 @@ class Dom
|
||||
* is exported with an XML declaration/
|
||||
* full HTML markup even if the input
|
||||
* didn't have them
|
||||
* @return string
|
||||
*/
|
||||
public function toString(bool $normalize = false): string
|
||||
{
|
||||
@@ -613,13 +637,16 @@ class Dom
|
||||
/**
|
||||
* Removes a node from the document but keeps its children
|
||||
* by moving them one level up
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* @return void
|
||||
*/
|
||||
public static function unwrap(DOMNode $node): void
|
||||
{
|
||||
foreach ($node->childNodes as $childNode) {
|
||||
// discard text nodes as they can be unexpected
|
||||
// directly in the parent element
|
||||
if ($childNode instanceof DOMText) {
|
||||
if (is_a($childNode, 'DOMText') === true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -635,6 +662,7 @@ class Dom
|
||||
* @param bool $normalize If set to `true`, the document
|
||||
* is exported with full HTML markup
|
||||
* even if the input didn't have it
|
||||
* @return string
|
||||
*/
|
||||
protected function exportHtml(bool $normalize = false): string
|
||||
{
|
||||
@@ -643,7 +671,7 @@ class Dom
|
||||
$metaTag = $this->doc->createElement('meta');
|
||||
$metaTag->setAttribute('http-equiv', 'Content-Type');
|
||||
$metaTag->setAttribute('content', 'text/html; charset=utf-8');
|
||||
$metaTag->setAttribute('id', Str::random(10));
|
||||
$metaTag->setAttribute('id', $metaId = Str::random(10));
|
||||
$this->doc->insertBefore($metaTag, $this->doc->documentElement);
|
||||
|
||||
if (
|
||||
@@ -674,13 +702,11 @@ class Dom
|
||||
* @param bool $normalize If set to `true`, the document
|
||||
* is exported with an XML declaration
|
||||
* even if the input didn't have it
|
||||
* @return string
|
||||
*/
|
||||
protected function exportXml(bool $normalize = false): string
|
||||
{
|
||||
if (
|
||||
Str::contains($this->code, '<?xml ', true) === false &&
|
||||
$normalize === false
|
||||
) {
|
||||
if (Str::contains($this->code, '<?xml ', true) === false && $normalize === false) {
|
||||
// the input didn't contain an XML declaration;
|
||||
// only return child nodes, which omits it
|
||||
$result = [];
|
||||
@@ -694,52 +720,23 @@ class Dom
|
||||
// ensure that the document is encoded as UTF-8
|
||||
// unless a different encoding was specified in
|
||||
// the input or before exporting
|
||||
$this->doc->encoding ??= 'UTF-8';
|
||||
if ($this->doc->encoding === null) {
|
||||
$this->doc->encoding = 'UTF-8';
|
||||
}
|
||||
|
||||
return trim($this->doc->saveXML());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that all options are set in the user-provided
|
||||
* options array (otherwise setting the default option)
|
||||
*/
|
||||
protected static function normalizeSanitizeOptions(array $options): array
|
||||
{
|
||||
// increase performance for already normalized option arrays
|
||||
if (($options['_normalized'] ?? false) === true) {
|
||||
return $options;
|
||||
}
|
||||
|
||||
return [
|
||||
'allowedAttrPrefixes' => [],
|
||||
'allowedAttrs' => true,
|
||||
'allowedDataUris' => true,
|
||||
'allowedDomains' => true,
|
||||
'allowHostRelativeUrls' => true,
|
||||
'allowedNamespaces' => true,
|
||||
'allowedPIs' => true,
|
||||
'allowedTags' => true,
|
||||
'attrCallback' => null,
|
||||
'disallowedTags' => [],
|
||||
'doctypeCallback' => null,
|
||||
'elementCallback' => null,
|
||||
'urlAttrs' => ['href', 'src', 'xlink:href'],
|
||||
...$options,
|
||||
'_normalized' => true
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes an attribute
|
||||
*
|
||||
* @param \DOMAttr $attr
|
||||
* @param array $options See `Dom::sanitize()`
|
||||
* @param array $errors Array to store additional errors in by reference
|
||||
* @return void
|
||||
*/
|
||||
protected function sanitizeAttr(
|
||||
DOMAttr $attr,
|
||||
array $options,
|
||||
array &$errors
|
||||
): void {
|
||||
protected function sanitizeAttr(DOMAttr $attr, array $options, array &$errors): void
|
||||
{
|
||||
$element = $attr->ownerElement;
|
||||
$name = $attr->nodeName;
|
||||
$value = $attr->value;
|
||||
@@ -781,14 +778,13 @@ class Dom
|
||||
/**
|
||||
* Sanitizes the doctype
|
||||
*
|
||||
* @param \DOMDocumentType $doctype
|
||||
* @param array $options See `Dom::sanitize()`
|
||||
* @param array $errors Array to store additional errors in by reference
|
||||
* @return void
|
||||
*/
|
||||
protected function sanitizeDoctype(
|
||||
DOMDocumentType $doctype,
|
||||
array $options,
|
||||
array &$errors
|
||||
): void {
|
||||
protected function sanitizeDoctype(DOMDocumentType $doctype, array $options, array &$errors): void
|
||||
{
|
||||
try {
|
||||
$this->validateDoctype($doctype, $options);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
@@ -800,14 +796,13 @@ class Dom
|
||||
/**
|
||||
* Sanitizes a single DOM element and its attribute
|
||||
*
|
||||
* @param \DOMElement $element
|
||||
* @param array $options See `Dom::sanitize()`
|
||||
* @param array $errors Array to store additional errors in by reference
|
||||
* @return void
|
||||
*/
|
||||
protected function sanitizeElement(
|
||||
DOMElement $element,
|
||||
array $options,
|
||||
array &$errors
|
||||
): void {
|
||||
protected function sanitizeElement(DOMElement $element, array $options, array &$errors): void
|
||||
{
|
||||
$name = $element->nodeName;
|
||||
|
||||
// check defined namespaces (`xmlns` attributes);
|
||||
@@ -869,28 +864,27 @@ class Dom
|
||||
|
||||
// custom check (if the attribute is still in the document)
|
||||
if ($attr->ownerElement !== null && $options['attrCallback']) {
|
||||
$errors = array_merge($errors, $options['attrCallback']($attr, $options) ?? []);
|
||||
$errors = array_merge($errors, $options['attrCallback']($attr) ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// custom check
|
||||
if ($options['elementCallback']) {
|
||||
$errors = array_merge($errors, $options['elementCallback']($element, $options) ?? []);
|
||||
$errors = array_merge($errors, $options['elementCallback']($element) ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a single XML processing instruction
|
||||
*
|
||||
* @param \DOMProcessingInstruction $pi
|
||||
* @param array $options See `Dom::sanitize()`
|
||||
* @param array $errors Array to store additional errors in by reference
|
||||
* @return void
|
||||
*/
|
||||
protected function sanitizePI(
|
||||
DOMProcessingInstruction $pi,
|
||||
array $options,
|
||||
array &$errors
|
||||
): void {
|
||||
protected function sanitizePI(DOMProcessingInstruction $pi, array $options, array &$errors): void
|
||||
{
|
||||
$name = $pi->nodeName;
|
||||
|
||||
// check for allow-listed processing instructions
|
||||
@@ -906,18 +900,15 @@ class Dom
|
||||
/**
|
||||
* Validates the document type
|
||||
*
|
||||
* @param \DOMDocumentType $doctype
|
||||
* @param array $options See `Dom::sanitize()`
|
||||
* @return void
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the doctype is not valid
|
||||
*/
|
||||
protected function validateDoctype(
|
||||
DOMDocumentType $doctype,
|
||||
array $options
|
||||
): void {
|
||||
if (
|
||||
empty($doctype->publicId) === false ||
|
||||
empty($doctype->systemId) === false
|
||||
) {
|
||||
protected function validateDoctype(DOMDocumentType $doctype, array $options): void
|
||||
{
|
||||
if (empty($doctype->publicId) === false || empty($doctype->systemId) === false) {
|
||||
throw new InvalidArgumentException('The doctype must not reference external files');
|
||||
}
|
||||
|
||||
@@ -926,7 +917,7 @@ class Dom
|
||||
}
|
||||
|
||||
if ($options['doctypeCallback']) {
|
||||
$options['doctypeCallback']($doctype, $options);
|
||||
$options['doctypeCallback']($doctype);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,10 @@ class Escape
|
||||
{
|
||||
/**
|
||||
* The internal singleton escaper instance
|
||||
*
|
||||
* @var \Laminas\Escaper\Escaper
|
||||
*/
|
||||
protected static Escaper|null $escaper = null;
|
||||
protected static $escaper;
|
||||
|
||||
/**
|
||||
* Escape common HTML attributes data
|
||||
@@ -42,8 +44,11 @@ class Escape
|
||||
* <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 $string): string
|
||||
public static function attr($string)
|
||||
{
|
||||
return static::escaper()->escapeHtmlAttr($string);
|
||||
}
|
||||
@@ -60,16 +65,21 @@ class Escape
|
||||
* <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 $string): string
|
||||
public static function css($string)
|
||||
{
|
||||
return static::escaper()->escapeCss($string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the escaper instance (and create if needed)
|
||||
*
|
||||
* @return \Laminas\Escaper\Escaper
|
||||
*/
|
||||
protected static function escaper(): Escaper
|
||||
protected static function escaper()
|
||||
{
|
||||
return static::$escaper ??= new Escaper('utf-8');
|
||||
}
|
||||
@@ -85,8 +95,11 @@ class Escape
|
||||
*
|
||||
* <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 $string): string
|
||||
public static function html($string)
|
||||
{
|
||||
return static::escaper()->escapeHtml($string);
|
||||
}
|
||||
@@ -100,8 +113,11 @@ class Escape
|
||||
* <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 $string): string
|
||||
public static function js($string)
|
||||
{
|
||||
return static::escaper()->escapeJs($string);
|
||||
}
|
||||
@@ -113,8 +129,11 @@ class Escape
|
||||
* 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 $string): string
|
||||
public static function url($string)
|
||||
{
|
||||
return rawurlencode($string);
|
||||
}
|
||||
@@ -132,8 +151,11 @@ class Escape
|
||||
* & is replaced with &
|
||||
* < is replaced with <
|
||||
* > is replaced with >
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function xml(string $string): string
|
||||
public static function xml($string)
|
||||
{
|
||||
return htmlspecialchars($string, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||
}
|
||||
|
||||
@@ -17,11 +17,17 @@ abstract class Facade
|
||||
/**
|
||||
* Returns the instance that should be
|
||||
* available statically
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public 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)
|
||||
{
|
||||
|
||||
@@ -19,13 +19,17 @@ class Html extends Xml
|
||||
{
|
||||
/**
|
||||
* An internal store for an HTML entities translation table
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static array|null $entities = null;
|
||||
public static $entities;
|
||||
|
||||
/**
|
||||
* List of HTML tags that can be used inline
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static array $inlineList = [
|
||||
public static $inlineList = [
|
||||
'b',
|
||||
'i',
|
||||
'small',
|
||||
@@ -90,11 +94,10 @@ class Html extends Xml
|
||||
*
|
||||
* @param string $tag Tag name
|
||||
* @param array $arguments Further arguments for the Html::tag() method
|
||||
* @return string
|
||||
*/
|
||||
public static function __callStatic(
|
||||
string $tag,
|
||||
array $arguments = []
|
||||
): string {
|
||||
public static function __callStatic(string $tag, array $arguments = []): string
|
||||
{
|
||||
if (static::isVoid($tag) === true) {
|
||||
return static::tag($tag, null, ...$arguments);
|
||||
}
|
||||
@@ -134,22 +137,13 @@ class Html extends Xml
|
||||
* @param string|null $after An optional string that will be appended if the result is not empty
|
||||
* @return string|null The generated HTML attributes string
|
||||
*/
|
||||
public static function attr(
|
||||
string|array $name,
|
||||
$value = null,
|
||||
string|null $before = null,
|
||||
string|null $after = null
|
||||
): string|null {
|
||||
public static function attr($name, $value = null, ?string $before = null, ?string $after = null): ?string
|
||||
{
|
||||
// HTML supports boolean attributes without values
|
||||
if (is_array($name) === false && is_bool($value) === true) {
|
||||
return $value === true ? strtolower($name) : null;
|
||||
}
|
||||
|
||||
// HTML attribute names are case-insensitive
|
||||
if (is_string($name) === true) {
|
||||
$name = strtolower($name);
|
||||
}
|
||||
|
||||
// all other cases can share the XML variant
|
||||
$attr = parent::attr($name, $value);
|
||||
|
||||
@@ -172,6 +166,9 @@ class Html extends Xml
|
||||
|
||||
/**
|
||||
* Converts lines in a string into HTML breaks
|
||||
*
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public static function breaks(string $string): string
|
||||
{
|
||||
@@ -186,11 +183,8 @@ class Html extends Xml
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function email(
|
||||
string $email,
|
||||
string|array|null $text = null,
|
||||
array $attr = []
|
||||
): string {
|
||||
public static function email(string $email, $text = null, array $attr = []): string
|
||||
{
|
||||
if (empty($email) === true) {
|
||||
return '';
|
||||
}
|
||||
@@ -219,15 +213,14 @@ class Html extends Xml
|
||||
/**
|
||||
* Converts a string to an HTML-safe string
|
||||
*
|
||||
* @param string|null $string
|
||||
* @param bool $keepTags If true, existing tags won't be escaped
|
||||
* @return string The HTML string
|
||||
*
|
||||
* @psalm-suppress ParamNameMismatch
|
||||
*/
|
||||
public static function encode(
|
||||
string|null $string,
|
||||
bool $keepTags = false
|
||||
): string {
|
||||
public static function encode(?string $string, bool $keepTags = false): string
|
||||
{
|
||||
if ($string === null) {
|
||||
return '';
|
||||
}
|
||||
@@ -247,6 +240,8 @@ class Html extends Xml
|
||||
|
||||
/**
|
||||
* Returns the entity translation table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function entities(): array
|
||||
{
|
||||
@@ -261,11 +256,8 @@ class Html extends Xml
|
||||
* @param array $attr Additional attributes for the `<figure>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function figure(
|
||||
string|array $content,
|
||||
string|array|null $caption = '',
|
||||
array $attr = []
|
||||
): string {
|
||||
public static function figure($content, $caption = '', array $attr = []): string
|
||||
{
|
||||
if ($caption) {
|
||||
$figcaption = static::tag('figcaption', $caption);
|
||||
|
||||
@@ -287,15 +279,12 @@ class Html extends Xml
|
||||
* @param array $attr Additional attributes for the `<script>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function gist(
|
||||
string $url,
|
||||
string|null $file = null,
|
||||
array $attr = []
|
||||
): string {
|
||||
$src = $url . '.js';
|
||||
|
||||
if ($file !== null) {
|
||||
$src .= '?file=' . $file;
|
||||
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', '', array_merge($attr, ['src' => $src]));
|
||||
@@ -304,6 +293,7 @@ class Html extends Xml
|
||||
/**
|
||||
* Creates an `<iframe>`
|
||||
*
|
||||
* @param string $src
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
@@ -323,7 +313,7 @@ class Html extends Xml
|
||||
{
|
||||
$attr = array_merge([
|
||||
'src' => $src,
|
||||
'alt' => ''
|
||||
'alt' => ' '
|
||||
], $attr);
|
||||
|
||||
return static::tag('img', '', $attr);
|
||||
@@ -331,6 +321,9 @@ class Html extends Xml
|
||||
|
||||
/**
|
||||
* Checks if a tag is self-closing
|
||||
*
|
||||
* @param string $tag
|
||||
* @return bool
|
||||
*/
|
||||
public static function isVoid(string $tag): bool
|
||||
{
|
||||
@@ -345,11 +338,8 @@ class Html extends Xml
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function link(
|
||||
string $href,
|
||||
string|array|null $text = null,
|
||||
array $attr = []
|
||||
): string {
|
||||
public static function link(string $href, $text = null, array $attr = []): string
|
||||
{
|
||||
$attr = array_merge(['href' => $href], $attr);
|
||||
|
||||
if (empty($text) === true) {
|
||||
@@ -367,16 +357,14 @@ class Html extends Xml
|
||||
}
|
||||
|
||||
/**
|
||||
* Add noreferrer to rels when target is `_blank`
|
||||
* Add noopener & noreferrer to rels when target is `_blank`
|
||||
*
|
||||
* @param string|null $rel Current `rel` value
|
||||
* @param string|null $target Current `target` value
|
||||
* @return string|null New `rel` value or `null` if not needed
|
||||
*/
|
||||
public static function rel(
|
||||
string|null $rel = null,
|
||||
string|null $target = null
|
||||
): string|null {
|
||||
public static function rel(?string $rel = null, ?string $target = null): ?string
|
||||
{
|
||||
$rel = trim($rel ?? '');
|
||||
|
||||
if ($target === '_blank') {
|
||||
@@ -384,10 +372,10 @@ class Html extends Xml
|
||||
return $rel;
|
||||
}
|
||||
|
||||
return trim($rel . ' noreferrer', ' ');
|
||||
return trim($rel . ' noopener noreferrer', ' ');
|
||||
}
|
||||
|
||||
return $rel ?: null;
|
||||
return $rel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,16 +389,13 @@ class Html extends Xml
|
||||
* @param int $level Indentation level
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function tag(
|
||||
string $name,
|
||||
array|string|null $content = '',
|
||||
array $attr = [],
|
||||
string $indent = null,
|
||||
int $level = 0
|
||||
): string {
|
||||
public static function tag(string $name, $content = '', array $attr = null, string $indent = null, int $level = 0): string
|
||||
{
|
||||
// treat an explicit `null` value as an empty tag
|
||||
// as void tags are already covered below
|
||||
$content ??= '';
|
||||
if ($content === null) {
|
||||
$content = '';
|
||||
}
|
||||
|
||||
// force void elements to be self-closing
|
||||
if (static::isVoid($name) === true) {
|
||||
@@ -428,11 +413,8 @@ class Html extends Xml
|
||||
* @param array $attr Additional attributes for the tag
|
||||
* @return string The generated HTML
|
||||
*/
|
||||
public static function tel(
|
||||
string $tel,
|
||||
string|array|null $text = null,
|
||||
array $attr = []
|
||||
): string {
|
||||
public static function tel(string $tel, $text = null, array $attr = []): string
|
||||
{
|
||||
$number = preg_replace('![^0-9\+]+!', '', $tel);
|
||||
|
||||
if (empty($text) === true) {
|
||||
@@ -444,8 +426,11 @@ class Html extends Xml
|
||||
|
||||
/**
|
||||
* Properly encodes tag contents
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string|null
|
||||
*/
|
||||
public static function value($value): string|null
|
||||
public static function value($value): ?string
|
||||
{
|
||||
if ($value === true) {
|
||||
return 'true';
|
||||
@@ -477,11 +462,8 @@ class Html extends Xml
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string|null The generated HTML
|
||||
*/
|
||||
public static function video(
|
||||
string $url,
|
||||
array $options = [],
|
||||
array $attr = []
|
||||
): string|null {
|
||||
public static function video(string $url, array $options = [], array $attr = []): ?string
|
||||
{
|
||||
// YouTube video
|
||||
if (Str::contains($url, 'youtu', true) === true) {
|
||||
return static::youtube($url, $options['youtube'] ?? [], $attr);
|
||||
@@ -513,6 +495,9 @@ class Html extends Xml
|
||||
/**
|
||||
* Generates a list of attributes
|
||||
* for video iframes
|
||||
*
|
||||
* @param array $attr
|
||||
* @return array
|
||||
*/
|
||||
public static function videoAttr(array $attr = []): array
|
||||
{
|
||||
@@ -537,20 +522,22 @@ class Html extends Xml
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string|null The generated HTML
|
||||
*/
|
||||
public static function vimeo(
|
||||
string $url,
|
||||
array $options = [],
|
||||
array $attr = []
|
||||
): string|null {
|
||||
public static function vimeo(string $url, array $options = [], array $attr = []): ?string
|
||||
{
|
||||
$uri = new Uri($url);
|
||||
$path = $uri->path();
|
||||
$query = $uri->query();
|
||||
$id = null;
|
||||
|
||||
$id = match ($uri->host()) {
|
||||
'vimeo.com', 'www.vimeo.com' => $path->last(),
|
||||
'player.vimeo.com' => $path->nth(1),
|
||||
default => null
|
||||
};
|
||||
switch ($uri->host()) {
|
||||
case 'vimeo.com':
|
||||
case 'www.vimeo.com':
|
||||
$id = $path->last();
|
||||
break;
|
||||
case 'player.vimeo.com':
|
||||
$id = $path->nth(1);
|
||||
break;
|
||||
}
|
||||
|
||||
if (empty($id) === true || preg_match('!^[0-9]*$!', $id) !== 1) {
|
||||
return null;
|
||||
@@ -575,11 +562,8 @@ class Html extends Xml
|
||||
* @param array $attr Additional attributes for the `<iframe>` tag
|
||||
* @return string|null The generated HTML
|
||||
*/
|
||||
public static function youtube(
|
||||
string $url,
|
||||
array $options = [],
|
||||
array $attr = []
|
||||
): string|null {
|
||||
public static function youtube(string $url, array $options = [], array $attr = []): ?string
|
||||
{
|
||||
if (preg_match('!youtu!i', $url) !== 1) {
|
||||
return null;
|
||||
}
|
||||
@@ -592,12 +576,12 @@ class Html extends Xml
|
||||
$host = 'https://' . $uri->host() . '/embed';
|
||||
$src = null;
|
||||
|
||||
$isYoutubeId = function (string|null $id = null): bool {
|
||||
$isYoutubeId = function (?string $id = null): bool {
|
||||
if (empty($id) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match('!^[a-zA-Z0-9_-]+$!', $id) === 1;
|
||||
return preg_match('!^[a-zA-Z0-9_-]+$!', $id);
|
||||
};
|
||||
|
||||
switch ($path->toString()) {
|
||||
@@ -623,19 +607,14 @@ class Html extends Xml
|
||||
|
||||
default:
|
||||
// short URLs
|
||||
if (
|
||||
Str::contains($uri->host(), 'youtu.be') === true &&
|
||||
$isYoutubeId($first) === true
|
||||
) {
|
||||
if (Str::contains($uri->host(), 'youtu.be') === true && $isYoutubeId($first) === true) {
|
||||
$src = 'https://www.youtube.com/embed/' . $first;
|
||||
|
||||
$query->start = $query->t;
|
||||
unset($query->t);
|
||||
} elseif (
|
||||
in_array($first, ['embed', 'shorts']) === true &&
|
||||
$isYoutubeId($second) === true
|
||||
) {
|
||||
// embedded and shorts video URLs
|
||||
|
||||
// embedded video URLs
|
||||
} elseif ($first === 'embed' && $isYoutubeId($second) === true) {
|
||||
$src = $host . '/' . $second;
|
||||
}
|
||||
}
|
||||
@@ -650,7 +629,7 @@ class Html extends Xml
|
||||
}
|
||||
|
||||
// build the full video src URL
|
||||
$src .= $query->toString(true);
|
||||
$src = $src . $query->toString(true);
|
||||
|
||||
// render the iframe
|
||||
return static::iframe($src, static::videoAttr($attr));
|
||||
|
||||
@@ -18,53 +18,68 @@ class I18n
|
||||
{
|
||||
/**
|
||||
* Custom loader function
|
||||
*
|
||||
* @var Closure
|
||||
*/
|
||||
public static Closure|null $load = null;
|
||||
public static $load = null;
|
||||
|
||||
/**
|
||||
* Current locale
|
||||
*
|
||||
* @var string|\Closure
|
||||
*/
|
||||
public static string|Closure|null $locale = 'en';
|
||||
public static $locale = 'en';
|
||||
|
||||
/**
|
||||
* All registered translations
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static array $translations = [];
|
||||
public static $translations = [];
|
||||
|
||||
/**
|
||||
* The fallback locale or a list of fallback locales
|
||||
* The fallback locale or a
|
||||
* list of fallback locales
|
||||
*
|
||||
* @var string|array|\Closure
|
||||
*/
|
||||
public static string|array|Closure|null $fallback = ['en'];
|
||||
public static $fallback = ['en'];
|
||||
|
||||
/**
|
||||
* Cache of `NumberFormatter` objects by locale
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static array $decimalsFormatters = [];
|
||||
protected static $decimalsFormatters = [];
|
||||
|
||||
/**
|
||||
* Returns the list of fallback locales
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function fallbacks(): array
|
||||
{
|
||||
if (is_callable(static::$fallback) === true) {
|
||||
static::$fallback = (static::$fallback)();
|
||||
}
|
||||
|
||||
if (is_array(static::$fallback) === true) {
|
||||
return static::$fallback;
|
||||
}
|
||||
|
||||
if (is_string(static::$fallback) === true) {
|
||||
if (
|
||||
is_array(static::$fallback) === true ||
|
||||
is_string(static::$fallback) === true
|
||||
) {
|
||||
return A::wrap(static::$fallback);
|
||||
}
|
||||
|
||||
if (is_callable(static::$fallback) === true) {
|
||||
return static::$fallback = A::wrap((static::$fallback)());
|
||||
}
|
||||
|
||||
return static::$fallback = ['en'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns singular or plural depending on the given number
|
||||
* Returns singular or plural
|
||||
* depending on the given number
|
||||
*
|
||||
* @param int $count
|
||||
* @param bool $none If true, 'none' will be returned if the count is 0
|
||||
* @return string
|
||||
*/
|
||||
public static function form(int $count, bool $none = false): string
|
||||
{
|
||||
@@ -77,41 +92,105 @@ class I18n
|
||||
|
||||
/**
|
||||
* Formats a number
|
||||
*
|
||||
* @param int|float $number
|
||||
* @param string $locale
|
||||
* @return string
|
||||
*/
|
||||
public static function formatNumber(int|float $number, string $locale = null): string
|
||||
public static function formatNumber($number, string $locale = null): string
|
||||
{
|
||||
$locale ??= static::locale();
|
||||
$locale ??= static::locale();
|
||||
|
||||
$formatter = static::decimalNumberFormatter($locale);
|
||||
$number = $formatter?->format($number) ?? $number;
|
||||
if ($formatter !== null) {
|
||||
$number = $formatter->format($number);
|
||||
}
|
||||
|
||||
return (string)$number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current locale code
|
||||
* Returns the locale code
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function locale(): string
|
||||
{
|
||||
if (is_callable(static::$locale) === true) {
|
||||
static::$locale = (static::$locale)();
|
||||
}
|
||||
|
||||
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 ??= static::locale();
|
||||
|
||||
if (is_array($key) === true) {
|
||||
// try to use actual locale
|
||||
if (isset($key[$locale])) {
|
||||
return $key[$locale];
|
||||
}
|
||||
// try to use language code, e.g. `es` when locale is `es_ES`
|
||||
$lang = Str::before($locale, '_');
|
||||
if (isset($key[$lang])) {
|
||||
return $key[$lang];
|
||||
}
|
||||
// use fallback
|
||||
if (is_array($fallback)) {
|
||||
return $fallback[$locale] ?? $fallback['en'] ?? reset($fallback);
|
||||
}
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
if ($translation = static::translation($locale)[$key] ?? null) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
if ($fallback !== null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
foreach (static::fallbacks() as $fallback) {
|
||||
// skip locales we have already tried
|
||||
if ($locale === $fallback) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($translation = static::translation($fallback)[$key] ?? null) {
|
||||
return $translation;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate by key and then replace
|
||||
* placeholders in the text
|
||||
*
|
||||
* @param string $key
|
||||
* @param string|array|null $fallback
|
||||
* @param array|null $replace
|
||||
* @param string|null $locale
|
||||
* @return string
|
||||
*/
|
||||
public static function template(
|
||||
string $key,
|
||||
string|array $fallback = null,
|
||||
array|null $replace = null,
|
||||
string|null $locale = null
|
||||
): string {
|
||||
public static function template(string $key, $fallback = null, ?array $replace = null, ?string $locale = null): string
|
||||
{
|
||||
if (is_array($fallback) === true) {
|
||||
$replace = $fallback;
|
||||
$fallback = null;
|
||||
@@ -119,144 +198,37 @@ class I18n
|
||||
}
|
||||
|
||||
$template = static::translate($key, $fallback, $locale);
|
||||
|
||||
return Str::template($template, $replace, ['fallback' => '-']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates either a given i18n key from global translations
|
||||
* or chooses correct entry from array of translations
|
||||
* according to the currently set locale
|
||||
*/
|
||||
public static function translate(
|
||||
string|array|null $key,
|
||||
string|array $fallback = null,
|
||||
string $locale = null
|
||||
): string|array|Closure|null {
|
||||
// use current locale if no specific is passed
|
||||
$locale ??= static::locale();
|
||||
// create shorter locale code, e.g. `es` for `es_ES` locale
|
||||
$shortLocale = Str::before($locale, '_');
|
||||
|
||||
// There are two main use cases that we will treat separately:
|
||||
// (1) with a string representing an i18n key to be looked up
|
||||
// (2) an array with entries per locale
|
||||
//
|
||||
// Both with various ways of handling fallbacks, provided
|
||||
// explicitly via the parameter and/or from global defaults.
|
||||
|
||||
// (1) string $key: look up i18n string from global translations
|
||||
if (is_string($key) === true) {
|
||||
// look up locale in global translations list,
|
||||
if ($result = static::translation($locale)[$key] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// prefer any direct provided $fallback
|
||||
// over further fallback alternatives
|
||||
if ($fallback !== null) {
|
||||
if (is_array($fallback) === true) {
|
||||
return static::translate($fallback, null, $locale);
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
// last resort: try using the fallback locales
|
||||
foreach (static::fallbacks() as $fallback) {
|
||||
// skip locale if we have already tried to save performance
|
||||
if ($locale === $fallback) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($result = static::translation($fallback)[$key] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --------
|
||||
// (2) array|null $key with entries per locale
|
||||
|
||||
// try entry for long and short locale
|
||||
if ($result = $key[$locale] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $key[$shortLocale] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// if the array as a global wildcard entry,
|
||||
// use this one as i18n key and try to resolve
|
||||
// this via part (1) of this method
|
||||
if ($wildcard = $key['*'] ?? null) {
|
||||
if ($result = static::translate($wildcard, $wildcard, $locale)) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// if the $fallback parameter is an array, we can assume
|
||||
// that it's also an array with entries per locale:
|
||||
// check with long and short locale if we find a matching entry
|
||||
if ($result = $fallback[$locale] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
if ($result = $fallback[$shortLocale] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// all options for long/short actual locale have been exhausted,
|
||||
// revert to the list of fallback locales and try with each of them
|
||||
foreach (static::fallbacks() as $locale) {
|
||||
// first on the original input
|
||||
if ($result = $key[$locale] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
// then on the fallback
|
||||
if ($result = $fallback[$locale] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// if a string was provided as fallback, use that one
|
||||
if (is_string($fallback) === true) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
// otherwise the first array element of the input
|
||||
// or the first array element of the fallback
|
||||
if (is_array($key) === true) {
|
||||
return reset($key);
|
||||
}
|
||||
if (is_array($fallback) === true) {
|
||||
return reset($fallback);
|
||||
}
|
||||
|
||||
return null;
|
||||
return Str::template($template, $replace, [
|
||||
'fallback' => '-',
|
||||
'start' => '{',
|
||||
'end' => '}'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ??= static::locale();
|
||||
|
||||
if ($translation = static::$translations[$locale] ?? null) {
|
||||
return $translation;
|
||||
if (isset(static::$translations[$locale]) === true) {
|
||||
return static::$translations[$locale];
|
||||
}
|
||||
|
||||
if (static::$load instanceof Closure) {
|
||||
if (is_a(static::$load, 'Closure') === true) {
|
||||
return static::$translations[$locale] = (static::$load)($locale);
|
||||
}
|
||||
|
||||
// try to use language code, e.g. `es` when locale is `es_ES`
|
||||
if ($translation = static::$translations[Str::before($locale, '_')] ?? null) {
|
||||
return $translation;
|
||||
$lang = Str::before($locale, '_');
|
||||
if (isset(static::$translations[$lang]) === true) {
|
||||
return static::$translations[$lang];
|
||||
}
|
||||
|
||||
return static::$translations[$locale] = [];
|
||||
@@ -264,6 +236,8 @@ class I18n
|
||||
|
||||
/**
|
||||
* Returns all loaded or defined translations
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function translations(): array
|
||||
{
|
||||
@@ -272,17 +246,16 @@ class I18n
|
||||
|
||||
/**
|
||||
* Returns (and creates) a decimal number formatter for a given locale
|
||||
*
|
||||
* @return \NumberFormatter|null
|
||||
*/
|
||||
protected static function decimalNumberFormatter(string $locale): NumberFormatter|null
|
||||
protected static function decimalNumberFormatter(string $locale): ?NumberFormatter
|
||||
{
|
||||
if ($formatter = static::$decimalsFormatters[$locale] ?? null) {
|
||||
return $formatter;
|
||||
if (isset(static::$decimalsFormatters[$locale])) {
|
||||
return static::$decimalsFormatters[$locale];
|
||||
}
|
||||
|
||||
if (
|
||||
extension_loaded('intl') !== true ||
|
||||
class_exists('NumberFormatter') !== true
|
||||
) {
|
||||
if (extension_loaded('intl') !== true || class_exists('NumberFormatter') !== true) {
|
||||
return null; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
@@ -299,14 +272,14 @@ class I18n
|
||||
* defined, the template that is defined last in the translation array is used
|
||||
* - Translation is a callback with a `$count` argument: Returns the callback return value
|
||||
*
|
||||
* @param string $key
|
||||
* @param int $count
|
||||
* @param string|null $locale
|
||||
* @param bool $formatNumber If set to `false`, the count is not formatted
|
||||
* @return mixed
|
||||
*/
|
||||
public static function translateCount(
|
||||
string $key,
|
||||
int $count,
|
||||
string $locale = null,
|
||||
bool $formatNumber = true
|
||||
) {
|
||||
public static function translateCount(string $key, int $count, string $locale = null, bool $formatNumber = true)
|
||||
{
|
||||
$locale ??= static::locale();
|
||||
$translation = static::translate($key, null, $locale);
|
||||
|
||||
@@ -314,20 +287,22 @@ class I18n
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($translation instanceof Closure) {
|
||||
if (is_a($translation, 'Closure') === true) {
|
||||
return $translation($count);
|
||||
}
|
||||
|
||||
$message = match (true) {
|
||||
is_string($translation) => $translation,
|
||||
isset($translation[$count]) => $translation[$count],
|
||||
default => end($translation)
|
||||
};
|
||||
if (is_string($translation) === true) {
|
||||
$message = $translation;
|
||||
} elseif (isset($translation[$count]) === true) {
|
||||
$message = $translation[$count];
|
||||
} else {
|
||||
$message = end($translation);
|
||||
}
|
||||
|
||||
if ($formatNumber === true) {
|
||||
$count = static::formatNumber($count, $locale);
|
||||
}
|
||||
|
||||
return Str::template($message, compact('count'));
|
||||
return str_replace('{{ count }}', $count, $message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,16 +15,21 @@ use IteratorAggregate;
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*
|
||||
* @psalm-suppress MissingTemplateParam Implementing template params
|
||||
* in this class would require
|
||||
* implementing them throughout
|
||||
* the code base: https://github.com/getkirby/kirby/pull/4886#pullrequestreview-1203577545
|
||||
*/
|
||||
class Iterator implements IteratorAggregate
|
||||
{
|
||||
public array $data = [];
|
||||
/**
|
||||
* The data array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $data = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
$this->data = $data;
|
||||
@@ -32,6 +37,8 @@ class Iterator implements IteratorAggregate
|
||||
|
||||
/**
|
||||
* Get an iterator for the items.
|
||||
*
|
||||
* @return \ArrayIterator
|
||||
*/
|
||||
public function getIterator(): ArrayIterator
|
||||
{
|
||||
@@ -40,14 +47,18 @@ class Iterator implements IteratorAggregate
|
||||
|
||||
/**
|
||||
* Returns the current key
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function key(): int|string|null
|
||||
public function key()
|
||||
{
|
||||
return key($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all keys
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
@@ -56,8 +67,10 @@ class Iterator implements IteratorAggregate
|
||||
|
||||
/**
|
||||
* Returns the current element
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function current(): mixed
|
||||
public function current()
|
||||
{
|
||||
return current($this->data);
|
||||
}
|
||||
@@ -65,8 +78,10 @@ class Iterator implements IteratorAggregate
|
||||
/**
|
||||
* Moves the cursor to the previous element
|
||||
* and returns it
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function prev(): mixed
|
||||
public function prev()
|
||||
{
|
||||
return prev($this->data);
|
||||
}
|
||||
@@ -74,8 +89,10 @@ class Iterator implements IteratorAggregate
|
||||
/**
|
||||
* Moves the cursor to the next element
|
||||
* and returns it
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function next(): mixed
|
||||
public function next()
|
||||
{
|
||||
return next($this->data);
|
||||
}
|
||||
@@ -83,13 +100,15 @@ class Iterator implements IteratorAggregate
|
||||
/**
|
||||
* Moves the cursor to the first element
|
||||
*/
|
||||
public function rewind(): void
|
||||
public function rewind()
|
||||
{
|
||||
reset($this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current element is valid
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function valid(): bool
|
||||
{
|
||||
@@ -98,6 +117,8 @@ class Iterator implements IteratorAggregate
|
||||
|
||||
/**
|
||||
* Counts all elements
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
@@ -110,7 +131,7 @@ class Iterator implements IteratorAggregate
|
||||
* @param mixed $needle the element to search for
|
||||
* @return int|false the index (int) of the element or false
|
||||
*/
|
||||
public function indexOf(mixed $needle): int|false
|
||||
public function indexOf($needle)
|
||||
{
|
||||
return array_search($needle, array_values($this->data));
|
||||
}
|
||||
@@ -119,32 +140,39 @@ class Iterator implements IteratorAggregate
|
||||
* Tries to find the key for the given element
|
||||
*
|
||||
* @param mixed $needle the element to search for
|
||||
* @return int|string|false the name of the key or false
|
||||
* @return string|false the name of the key or false
|
||||
*/
|
||||
public function keyOf(mixed $needle): int|string|false
|
||||
public function keyOf($needle)
|
||||
{
|
||||
return array_search($needle, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks by key if an element is included
|
||||
*
|
||||
* @param mixed $key
|
||||
* @return bool
|
||||
*/
|
||||
public function has(mixed $key): bool
|
||||
public function has($key): bool
|
||||
{
|
||||
return isset($this->data[$key]) === true;
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current key is set
|
||||
*
|
||||
* @param mixed $key the key to check
|
||||
* @return bool
|
||||
*/
|
||||
public function __isset(mixed $key): bool
|
||||
public function __isset($key): bool
|
||||
{
|
||||
return $this->has($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified var_dump output
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Store a lazy value (safe from processing inside a closure)
|
||||
* in this class wrapper to also protect it from being unwrapped
|
||||
* by normal `Closure`/`is_callable()` checks
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class LazyValue
|
||||
{
|
||||
public function __construct(
|
||||
protected Closure $value
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the lazy value to its actual value
|
||||
*/
|
||||
public function resolve(mixed ...$args): mixed
|
||||
{
|
||||
return call_user_func_array($this->value, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap a single value or an array of values
|
||||
*/
|
||||
public static function unwrap(mixed $data, mixed ...$args): mixed
|
||||
{
|
||||
if (is_array($data) === true) {
|
||||
return A::map($data, fn ($value) => static::unwrap($value, $args));
|
||||
}
|
||||
|
||||
if ($data instanceof static) {
|
||||
return $data->resolve(...$args);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -21,28 +21,32 @@ class Locale
|
||||
* List of all locale constants supported by PHP
|
||||
*/
|
||||
public const LOCALE_CONSTANTS = [
|
||||
'LC_COLLATE',
|
||||
'LC_CTYPE',
|
||||
'LC_MONETARY',
|
||||
'LC_NUMERIC',
|
||||
'LC_TIME',
|
||||
'LC_MESSAGES'
|
||||
'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY',
|
||||
'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'
|
||||
];
|
||||
|
||||
/**
|
||||
* Converts a normalized locale array to an array with the
|
||||
* locale constants replaced with their string representations
|
||||
*
|
||||
* @param array $locale
|
||||
* @return array
|
||||
*/
|
||||
public static function export(array $locale): array
|
||||
{
|
||||
$return = [];
|
||||
$constants = static::supportedConstants(true);
|
||||
|
||||
// replace the keys in the locale data array with the locale names
|
||||
$return = [];
|
||||
foreach ($locale as $key => $value) {
|
||||
// use string representation for key
|
||||
// if it is a valid constant
|
||||
$return[$constants[$key] ?? $key] = $value;
|
||||
if (isset($constants[$key]) === true) {
|
||||
// the key is a valid constant,
|
||||
// replace it with its string representation
|
||||
$return[$constants[$key]] = $value;
|
||||
} else {
|
||||
// not found, keep it as-is
|
||||
$return[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
@@ -59,7 +63,7 @@ class Locale
|
||||
* @throws \Kirby\Exception\Exception If the locale cannot be determined
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the provided locale category is invalid
|
||||
*/
|
||||
public static function get(int|string $category = LC_ALL): array|string
|
||||
public static function get($category = LC_ALL)
|
||||
{
|
||||
$normalizedCategory = static::normalizeConstant($category);
|
||||
|
||||
@@ -101,10 +105,11 @@ class Locale
|
||||
* string keys to a normalized constant => value array
|
||||
*
|
||||
* @param array|string $locale
|
||||
* @return array
|
||||
*/
|
||||
public static function normalize($locale): array
|
||||
{
|
||||
if (is_array($locale) === true) {
|
||||
if (is_array($locale)) {
|
||||
// replace string constant keys with the constant values
|
||||
$convertedLocale = [];
|
||||
foreach ($locale as $key => $value) {
|
||||
@@ -112,20 +117,21 @@ class Locale
|
||||
}
|
||||
|
||||
return $convertedLocale;
|
||||
}
|
||||
|
||||
if (is_string($locale) === true) {
|
||||
} elseif (is_string($locale)) {
|
||||
return [LC_ALL => $locale];
|
||||
} else {
|
||||
throw new InvalidArgumentException('Locale must be string or array');
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Locale must be string or array');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PHP locale with a locale string or
|
||||
* an array with constant or string keys
|
||||
*
|
||||
* @param array|string $locale
|
||||
* @return void
|
||||
*/
|
||||
public static function set(array|string $locale): void
|
||||
public static function set($locale): void
|
||||
{
|
||||
$locale = static::normalize($locale);
|
||||
|
||||
@@ -146,13 +152,13 @@ class Locale
|
||||
/**
|
||||
* Tries to convert an `LC_*` constant name
|
||||
* to its constant value
|
||||
*
|
||||
* @param int|string $constant
|
||||
* @return int|string
|
||||
*/
|
||||
protected static function normalizeConstant(int|string $constant): int|string
|
||||
protected static function normalizeConstant($constant)
|
||||
{
|
||||
if (
|
||||
is_string($constant) === true &&
|
||||
Str::startsWith($constant, 'LC_') === true
|
||||
) {
|
||||
if (is_string($constant) === true && Str::startsWith($constant, 'LC_') === true) {
|
||||
return constant($constant);
|
||||
}
|
||||
|
||||
@@ -165,6 +171,7 @@ class Locale
|
||||
* that are actually supported on this system
|
||||
*
|
||||
* @param bool $withAll If set to `true`, `LC_ALL` is returned as well
|
||||
* @return array
|
||||
*/
|
||||
protected static function supportedConstants(bool $withAll = false): array
|
||||
{
|
||||
|
||||
@@ -17,6 +17,11 @@ use stdClass;
|
||||
*/
|
||||
class Obj extends stdClass
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
foreach ($data as $key => $val) {
|
||||
@@ -26,6 +31,10 @@ class Obj extends stdClass
|
||||
|
||||
/**
|
||||
* Magic getter
|
||||
*
|
||||
* @param string $property
|
||||
* @param array $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $property, array $arguments)
|
||||
{
|
||||
@@ -34,7 +43,8 @@ class Obj extends stdClass
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
@@ -43,6 +53,9 @@ class Obj extends stdClass
|
||||
|
||||
/**
|
||||
* Magic property getter
|
||||
*
|
||||
* @param string $property
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get(string $property)
|
||||
{
|
||||
@@ -52,15 +65,19 @@ class Obj extends stdClass
|
||||
/**
|
||||
* Gets one or multiple properties of the object
|
||||
*
|
||||
* @param string|array $property
|
||||
* @param mixed $fallback If multiple properties are requested:
|
||||
* Associative array of fallback values per key
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string|array $property, $fallback = null)
|
||||
public function get($property, $fallback = null)
|
||||
{
|
||||
if (is_array($property)) {
|
||||
$fallback ??= [];
|
||||
if ($fallback === null) {
|
||||
$fallback = [];
|
||||
}
|
||||
|
||||
if (is_array($fallback) === false) {
|
||||
if (!is_array($fallback)) {
|
||||
throw new InvalidArgumentException('The fallback value must be an array when getting multiple properties');
|
||||
}
|
||||
|
||||
@@ -76,16 +93,15 @@ class Obj extends stdClass
|
||||
|
||||
/**
|
||||
* 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')
|
||||
) {
|
||||
if (is_object($value) === true && method_exists($value, 'toArray')) {
|
||||
$result[$key] = $value->toArray();
|
||||
} else {
|
||||
$result[$key] = $value;
|
||||
@@ -97,6 +113,9 @@ class Obj extends stdClass
|
||||
|
||||
/**
|
||||
* Converts the object to a json string
|
||||
*
|
||||
* @param mixed ...$arguments
|
||||
* @return string
|
||||
*/
|
||||
public function toJson(...$arguments): string
|
||||
{
|
||||
|
||||
@@ -16,105 +16,100 @@ use Kirby\Exception\Exception;
|
||||
*/
|
||||
class Pagination
|
||||
{
|
||||
use Properties {
|
||||
setProperties as protected baseSetProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current page
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $page = 1;
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* Total number of items
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $total = 0;
|
||||
protected $total = 0;
|
||||
|
||||
/**
|
||||
* The number of items per page
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected int $limit = 20;
|
||||
protected $limit = 20;
|
||||
|
||||
/**
|
||||
* Whether validation of the pagination page
|
||||
* is enabled; will throw Exceptions if true
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static bool $validate = true;
|
||||
public static $validate = true;
|
||||
|
||||
/**
|
||||
* Creates a new pagination object
|
||||
* with the given parameters
|
||||
*
|
||||
* @param array $props
|
||||
*/
|
||||
public function __construct(array $props = [])
|
||||
{
|
||||
$this->setLimit($props['limit'] ?? 20);
|
||||
$this->setPage($props['page'] ?? null);
|
||||
$this->setTotal($props['total'] ?? 0);
|
||||
|
||||
// ensure that page is set to something, otherwise
|
||||
// generate "default page" based on other params
|
||||
$this->page ??= $this->firstPage();
|
||||
|
||||
// allow a page value of 1 even if there are no pages;
|
||||
// otherwise the exception will get thrown for this pretty common case
|
||||
$min = $this->firstPage();
|
||||
$max = $this->pages();
|
||||
if ($this->page === 1 && $max === 0) {
|
||||
$this->page = 0;
|
||||
}
|
||||
|
||||
// validate page based on all params if validation is enabled,
|
||||
// otherwise limit the page number to the bounds
|
||||
if ($this->page < $min || $this->page > $max) {
|
||||
if (static::$validate === true) {
|
||||
throw new ErrorPageException('Pagination page ' . $this->page . ' does not exist, expected ' . $min . '-' . $max);
|
||||
}
|
||||
|
||||
$this->page = max(min($this->page, $max), $min);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance while
|
||||
* merging initial and new properties
|
||||
*/
|
||||
public function clone(array $props = []): static
|
||||
{
|
||||
return new static(array_replace_recursive([
|
||||
'page' => $this->page,
|
||||
'limit' => $this->limit,
|
||||
'total' => $this->total
|
||||
], $props));
|
||||
$this->setProperties($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pagination instance for the given
|
||||
* collection with a flexible argument api
|
||||
*
|
||||
* @param \Kirby\Toolkit\Collection $collection
|
||||
* @param mixed ...$arguments
|
||||
* @return static
|
||||
*/
|
||||
public static function for(Collection $collection, ...$arguments): static
|
||||
public static function for(Collection $collection, ...$arguments)
|
||||
{
|
||||
$a = $arguments[0] ?? null;
|
||||
$b = $arguments[1] ?? null;
|
||||
|
||||
$params = [];
|
||||
|
||||
// First argument is a pagination object
|
||||
if ($a instanceof static) {
|
||||
if (is_a($a, static::class) === true) {
|
||||
/**
|
||||
* First argument is a pagination/self object
|
||||
*/
|
||||
return $a;
|
||||
}
|
||||
|
||||
if (is_array($a) === true) {
|
||||
// First argument is an option array
|
||||
// $collection->paginate([...])
|
||||
} elseif (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)
|
||||
/**
|
||||
* 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)
|
||||
/**
|
||||
* 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, [...])
|
||||
/**
|
||||
* First argument is the limit,
|
||||
* second argument are options
|
||||
*
|
||||
* $collection->paginate(10, [...])
|
||||
*/
|
||||
$params = $b;
|
||||
$params['limit'] = $a;
|
||||
}
|
||||
@@ -131,6 +126,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Getter for the current page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function page(): int
|
||||
{
|
||||
@@ -139,6 +136,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Getter for the total number of items
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function total(): int
|
||||
{
|
||||
@@ -147,6 +146,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Getter for the number of items per page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function limit(): int
|
||||
{
|
||||
@@ -155,24 +156,40 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns the index of the first item on the page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function start(): int
|
||||
{
|
||||
$index = max(0, $this->page() - 1);
|
||||
$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 = min($this->total(), ($this->start() - 1) + $this->limit());
|
||||
return $value;
|
||||
$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
|
||||
{
|
||||
@@ -185,6 +202,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns the first page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function firstPage(): int
|
||||
{
|
||||
@@ -193,6 +212,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns the last page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function lastPage(): int
|
||||
{
|
||||
@@ -201,6 +222,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns the offset (i.e. for db queries)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function offset(): int
|
||||
{
|
||||
@@ -209,6 +232,9 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Checks if the given page exists
|
||||
*
|
||||
* @param int $page
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPage(int $page): bool
|
||||
{
|
||||
@@ -225,6 +251,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Checks if there are any pages at all
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPages(): bool
|
||||
{
|
||||
@@ -233,6 +261,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Checks if there's a previous page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPrevPage(): bool
|
||||
{
|
||||
@@ -241,14 +271,18 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns the previous page
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function prevPage(): int|null
|
||||
public function prevPage()
|
||||
{
|
||||
return $this->hasPrevPage() ? $this->page() - 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there's a next page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNextPage(): bool
|
||||
{
|
||||
@@ -257,14 +291,18 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns the next page
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function nextPage(): int|null
|
||||
public function nextPage()
|
||||
{
|
||||
return $this->hasNextPage() ? $this->page() + 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current page is the first page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFirstPage(): bool
|
||||
{
|
||||
@@ -273,6 +311,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Checks if the current page is the last page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLastPage(): bool
|
||||
{
|
||||
@@ -281,6 +321,9 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Creates a range of page numbers for Google-like pagination
|
||||
*
|
||||
* @param int $range
|
||||
* @return array
|
||||
*/
|
||||
public function range(int $range = 5): array
|
||||
{
|
||||
@@ -293,7 +336,7 @@ class Pagination
|
||||
return range($start, $end);
|
||||
}
|
||||
|
||||
$middle = (int)floor($range / 2);
|
||||
$middle = (int)floor($range/2);
|
||||
$start = $page - $middle + ($range % 2 === 0);
|
||||
$end = $start + $range - 1;
|
||||
|
||||
@@ -312,6 +355,9 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns the first page of the created range
|
||||
*
|
||||
* @param int $range
|
||||
* @return int
|
||||
*/
|
||||
public function rangeStart(int $range = 5): int
|
||||
{
|
||||
@@ -320,6 +366,9 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns the last page of the created range
|
||||
*
|
||||
* @param int $range
|
||||
* @return int
|
||||
*/
|
||||
public function rangeEnd(int $range = 5): int
|
||||
{
|
||||
@@ -328,11 +377,50 @@ class Pagination
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of items per page
|
||||
* Sets the properties limit, total and page
|
||||
* and validates that the properties match
|
||||
*
|
||||
* @param array $props Array with keys limit, total and/or page
|
||||
* @return $this
|
||||
*/
|
||||
protected function setLimit(int $limit = 20): static
|
||||
protected function setProperties(array $props)
|
||||
{
|
||||
$this->baseSetProperties($props);
|
||||
|
||||
// ensure that page is set to something, otherwise
|
||||
// generate "default page" based on other params
|
||||
if ($this->page === null) {
|
||||
$this->page = $this->firstPage();
|
||||
}
|
||||
|
||||
// allow a page value of 1 even if there are no pages;
|
||||
// otherwise the exception will get thrown for this pretty common case
|
||||
$min = $this->firstPage();
|
||||
$max = $this->pages();
|
||||
if ($this->page === 1 && $max === 0) {
|
||||
$this->page = 0;
|
||||
}
|
||||
|
||||
// validate page based on all params if validation is enabled,
|
||||
// otherwise limit the page number to the bounds
|
||||
if ($this->page < $min || $this->page > $max) {
|
||||
if (static::$validate === true) {
|
||||
throw new ErrorPageException('Pagination page ' . $this->page . ' does not exist, expected ' . $min . '-' . $max);
|
||||
} else {
|
||||
$this->page = max(min($this->page, $max), $min);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of items per page
|
||||
*
|
||||
* @param int $limit
|
||||
* @return $this
|
||||
*/
|
||||
protected function setLimit(int $limit = 20)
|
||||
{
|
||||
if ($limit < 1) {
|
||||
throw new Exception('Invalid pagination limit: ' . $limit);
|
||||
@@ -345,9 +433,10 @@ class Pagination
|
||||
/**
|
||||
* Sets the total number of items
|
||||
*
|
||||
* @param int $total
|
||||
* @return $this
|
||||
*/
|
||||
protected function setTotal(int $total = 0): static
|
||||
protected function setTotal(int $total = 0)
|
||||
{
|
||||
if ($total < 0) {
|
||||
throw new Exception('Invalid total number of items: ' . $total);
|
||||
@@ -364,7 +453,7 @@ class Pagination
|
||||
* automatically determined if null
|
||||
* @return $this
|
||||
*/
|
||||
protected function setPage(int|string|null $page = null): static
|
||||
protected function setPage($page = null)
|
||||
{
|
||||
// if $page is null, it is set to a default in the setProperties() method
|
||||
if ($page !== null) {
|
||||
@@ -380,6 +469,8 @@ class Pagination
|
||||
|
||||
/**
|
||||
* Returns an array with all properties
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
|
||||
@@ -7,8 +7,6 @@ use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Properties
|
||||
* @deprecated 4.0.0 Will be remove in Kirby 5
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
|
||||
244
kirby/src/Toolkit/Query.php
Normal file
244
kirby/src/Toolkit/Query.php
Normal file
@@ -0,0 +1,244 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Kirby\Exception\BadMethodCallException;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* 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 https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Query
|
||||
{
|
||||
public const PARTS = '!\.|(\(([^()]+|(?1))*+\))(*SKIP)(*FAIL)!'; // split by dot, but not inside (nested) parens
|
||||
public const PARAMETERS = '!,|' . self::SKIP . '!'; // split by comma, but not inside skip groups
|
||||
|
||||
public const NO_PNTH = '\([^(]+\)(*SKIP)(*FAIL)';
|
||||
public const NO_SQBR = '\[[^]]+\](*SKIP)(*FAIL)';
|
||||
public const NO_DLQU = '\"(?:[^"\\\\]|\\\\.)*\"(*SKIP)(*FAIL)'; // allow \" escaping inside string
|
||||
public const NO_SLQU = '\'(?:[^\'\\\\]|\\\\.)*\'(*SKIP)(*FAIL)'; // allow \' escaping inside string
|
||||
public const SKIP = self::NO_PNTH . '|' . self::NO_SQBR . '|' .
|
||||
self::NO_DLQU . '|' . self::NO_SLQU;
|
||||
|
||||
/**
|
||||
* The query string
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* Queryable data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Creates a new Query object
|
||||
*
|
||||
* @param string|null $query
|
||||
* @param array|object $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;
|
||||
}
|
||||
|
||||
return $this->resolve($this->query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the query if anything
|
||||
* can be found, otherwise returns null
|
||||
*
|
||||
* @param string $query
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Kirby\Exception\BadMethodCallException If an invalid method is accessed by the query
|
||||
*/
|
||||
protected function resolve(string $query)
|
||||
{
|
||||
// direct key access in arrays
|
||||
if (is_array($this->data) === true && array_key_exists($query, $this->data) === true) {
|
||||
return $this->data[$query];
|
||||
}
|
||||
|
||||
$parts = $this->parts($query);
|
||||
$data = $this->data;
|
||||
$value = null;
|
||||
|
||||
foreach ($parts as $part) {
|
||||
$info = $this->part($part);
|
||||
$method = $info['method'];
|
||||
$args = $info['args'];
|
||||
|
||||
if (is_array($data)) {
|
||||
if (array_key_exists($method, $data) === true) {
|
||||
$value = $data[$method];
|
||||
|
||||
if (is_a($value, 'Closure') === true) {
|
||||
$value = $value(...$args);
|
||||
} elseif ($args !== []) {
|
||||
throw new InvalidArgumentException('Cannot access array element ' . $method . ' with arguments');
|
||||
}
|
||||
} else {
|
||||
static::accessError($data, $method, 'property');
|
||||
}
|
||||
} elseif (is_object($data)) {
|
||||
if (
|
||||
method_exists($data, $method) === true ||
|
||||
method_exists($data, '__call') === true
|
||||
) {
|
||||
$value = $data->$method(...$args);
|
||||
} elseif (
|
||||
$args === [] && (
|
||||
property_exists($data, $method) === true ||
|
||||
method_exists($data, '__get') === true
|
||||
)
|
||||
) {
|
||||
$value = $data->$method;
|
||||
} else {
|
||||
$label = ($args === []) ? 'method/property' : 'method';
|
||||
static::accessError($data, $method, $label);
|
||||
}
|
||||
} else {
|
||||
// further parts on a scalar/null value
|
||||
static::accessError($data, $method, 'method/property');
|
||||
}
|
||||
|
||||
// continue with the current value for the next part
|
||||
$data = $value;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks the query string down into its components
|
||||
*
|
||||
* @param string $query
|
||||
* @return array
|
||||
*/
|
||||
protected function parts(string $query): array
|
||||
{
|
||||
return preg_split(self::PARTS, trim($query), -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes each part of the query string and
|
||||
* extracts methods and method arguments
|
||||
*
|
||||
* @param string $part
|
||||
* @return array
|
||||
*/
|
||||
protected function part(string $part): array
|
||||
{
|
||||
if (Str::endsWith($part, ')') === true) {
|
||||
$method = Str::before($part, '(');
|
||||
|
||||
// the args are everything inside the *outer* parentheses
|
||||
$args = Str::substr($part, Str::position($part, '(') + 1, -1);
|
||||
$args = preg_split(self::PARAMETERS, $args);
|
||||
$args = array_map('self::parameter', $args);
|
||||
|
||||
return compact('method', 'args');
|
||||
} else {
|
||||
return [
|
||||
'method' => $part,
|
||||
'args' => []
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a parameter of a query to
|
||||
* its proper native PHP type
|
||||
*
|
||||
* @param string $arg
|
||||
* @return mixed
|
||||
*/
|
||||
protected function parameter(string $arg)
|
||||
{
|
||||
$arg = trim($arg);
|
||||
|
||||
// string with double quotes
|
||||
if (substr($arg, 0, 1) === '"' && substr($arg, -1) === '"') {
|
||||
return str_replace('\"', '"', substr($arg, 1, -1));
|
||||
}
|
||||
|
||||
// string with single quotes
|
||||
if (substr($arg, 0, 1) === "'" && substr($arg, -1) === "'") {
|
||||
return str_replace("\'", "'", substr($arg, 1, -1));
|
||||
}
|
||||
|
||||
// boolean or null
|
||||
switch ($arg) {
|
||||
case 'null':
|
||||
return null;
|
||||
case 'false':
|
||||
return false;
|
||||
case 'true':
|
||||
return true;
|
||||
}
|
||||
|
||||
// numeric
|
||||
if (is_numeric($arg) === true) {
|
||||
return (float)$arg;
|
||||
}
|
||||
|
||||
// array: split and recursive sanitizing
|
||||
if (substr($arg, 0, 1) === '[' && substr($arg, -1) === ']') {
|
||||
$arg = substr($arg, 1, -1);
|
||||
$arg = preg_split(self::PARAMETERS, $arg);
|
||||
return array_map('self::parameter', $arg);
|
||||
}
|
||||
|
||||
// resolve parameter for objects and methods itself
|
||||
return $this->resolve($arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws an exception for an access to an invalid method
|
||||
*
|
||||
* @param mixed $data Variable on which the access was tried
|
||||
* @param string $name Name of the method/property that was accessed
|
||||
* @param string $label Type of the name (`method`, `property` or `method/property`)
|
||||
* @return void
|
||||
*
|
||||
* @throws \Kirby\Exception\BadMethodCallException
|
||||
*/
|
||||
protected static function accessError($data, string $name, string $label): void
|
||||
{
|
||||
$type = strtolower(gettype($data));
|
||||
if ($type === 'double') {
|
||||
$type = 'float';
|
||||
}
|
||||
|
||||
$nonExisting = in_array($type, ['array', 'object']) ? 'non-existing ' : '';
|
||||
|
||||
$error = 'Access to ' . $nonExisting . $label . ' ' . $name . ' on ' . $type;
|
||||
throw new BadMethodCallException($error);
|
||||
}
|
||||
}
|
||||
@@ -15,22 +15,34 @@ namespace Kirby\Toolkit;
|
||||
*/
|
||||
class Silo
|
||||
{
|
||||
public static array $data = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $data = [];
|
||||
|
||||
/**
|
||||
* Setter for new data
|
||||
* Setter for new data.
|
||||
*
|
||||
* @param string|array $key
|
||||
* @param mixed $value
|
||||
* @return array
|
||||
*/
|
||||
public static function set(string|array $key, $value = null): 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;
|
||||
}
|
||||
|
||||
static::$data[$key] = $value;
|
||||
return static::$data;
|
||||
}
|
||||
|
||||
public static function get(string|array $key = null, $default = null)
|
||||
/**
|
||||
* @param string|array $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get($key = null, $default = null)
|
||||
{
|
||||
if ($key === null) {
|
||||
return static::$data;
|
||||
@@ -41,6 +53,9 @@ class Silo
|
||||
|
||||
/**
|
||||
* Removes an item from the data array
|
||||
*
|
||||
* @param string|null $key
|
||||
* @return array
|
||||
*/
|
||||
public static function remove(string $key = null): array
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,216 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Kirby\Data\Json;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use SensitiveParameter;
|
||||
|
||||
/**
|
||||
* User-friendly and safe abstraction for symmetric
|
||||
* authenticated encryption and decryption using the
|
||||
* PHP `sodium` extension
|
||||
* @since 3.9.8
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class SymmetricCrypto
|
||||
{
|
||||
/**
|
||||
* Cache for secret keys derived from the password
|
||||
* indexed by the used salt and limits
|
||||
*/
|
||||
protected array $secretKeysByOptions = [];
|
||||
|
||||
/**
|
||||
* Initializes the keys used for crypto, both optional
|
||||
*
|
||||
* @param string|null $password Password to be derived into a `$secretKey`
|
||||
* @param string|null $secretKey 256-bit key, alternatively a `$password` can be used
|
||||
*/
|
||||
public function __construct(
|
||||
#[SensitiveParameter]
|
||||
protected string|null $password = null,
|
||||
#[SensitiveParameter]
|
||||
protected string|null $secretKey = null,
|
||||
) {
|
||||
if ($password !== null && $secretKey !== null) {
|
||||
throw new InvalidArgumentException('Passing both a secret key and a password is not supported');
|
||||
}
|
||||
|
||||
if ($secretKey !== null && strlen($secretKey) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
|
||||
throw new InvalidArgumentException('Invalid secret key length, expected ' . SODIUM_CRYPTO_SECRETBOX_KEYBYTES . ' bytes');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide values of secrets when printing the object
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return [
|
||||
'hasPassword' => isset($this->password),
|
||||
'hasSecretKey' => isset($this->secretKey),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes the secrets from memory when they are no longer needed
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->memzero($this->password);
|
||||
$this->memzero($this->secretKey);
|
||||
|
||||
foreach ($this->secretKeysByOptions as $key => &$value) {
|
||||
$this->memzero($value);
|
||||
unset($this->secretKeysByOptions[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts JSON data encrypted by `SymmetricCrypto::encrypt()` using the secret key or password
|
||||
*
|
||||
* <code>
|
||||
* // decryption with a password
|
||||
* $crypto = new SymmetricCrypto(password: 'super secure');
|
||||
* $plaintext = $crypto->decrypt('a very confidential string');
|
||||
*
|
||||
* // decryption with a previously generated key
|
||||
* $crypto = new SymmetricCrypto(secretKey: $secretKey);
|
||||
* $plaintext = $crypto->decrypt('{"mode":"secretbox"...}');
|
||||
* </code>
|
||||
*/
|
||||
public function decrypt(string $json): string
|
||||
{
|
||||
$props = Json::decode($json);
|
||||
|
||||
if (($props['mode'] ?? null) !== 'secretbox') {
|
||||
throw new InvalidArgumentException('Unsupported encryption mode "' . ($props['mode'] ?? '') . '"');
|
||||
}
|
||||
|
||||
if (
|
||||
isset($props['data']) !== true ||
|
||||
isset($props['nonce']) !== true ||
|
||||
isset($props['salt']) !== true ||
|
||||
isset($props['limits']) !== true
|
||||
) {
|
||||
throw new InvalidArgumentException('Input data does not contain all required props');
|
||||
}
|
||||
|
||||
$data = base64_decode($props['data']);
|
||||
$nonce = base64_decode($props['nonce']);
|
||||
$salt = base64_decode($props['salt']);
|
||||
$limits = $props['limits'];
|
||||
|
||||
$plaintext = sodium_crypto_secretbox_open($data, $nonce, $this->secretKey($salt, $limits));
|
||||
|
||||
if (is_string($plaintext) !== true) {
|
||||
throw new LogicException('Encrypted string was tampered with');
|
||||
}
|
||||
|
||||
return $plaintext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a string using the secret key or password
|
||||
*
|
||||
* <code>
|
||||
* // encryption with a password
|
||||
* $crypto = new SymmetricCrypto(password: 'super secure');
|
||||
* $ciphertext = $crypto->encrypt('a very confidential string');
|
||||
*
|
||||
* // encryption with a random key
|
||||
* $crypto = new SymmetricCrypto();
|
||||
* $ciphertext = $crypto->encrypt('a very confidential string');
|
||||
* $secretKey = $crypto->secretKey();
|
||||
* </code>
|
||||
*/
|
||||
public function encrypt(
|
||||
#[SensitiveParameter]
|
||||
string $string
|
||||
): string {
|
||||
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||
$salt = random_bytes(SODIUM_CRYPTO_PWHASH_SALTBYTES);
|
||||
$limits = [SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE];
|
||||
$key = $this->secretKey($salt, $limits);
|
||||
|
||||
$ciphertext = sodium_crypto_secretbox($string, $nonce, $key);
|
||||
|
||||
// bundle all necessary information in a JSON object;
|
||||
// always include the salt and limits to hide whether a key or password was used
|
||||
return Json::encode([
|
||||
'mode' => 'secretbox',
|
||||
'data' => base64_encode($ciphertext),
|
||||
'nonce' => base64_encode($nonce),
|
||||
'salt' => base64_encode($salt),
|
||||
'limits' => $limits,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the required PHP `sodium` extension is available
|
||||
*/
|
||||
public static function isAvailable(): bool
|
||||
{
|
||||
return defined('SODIUM_LIBRARY_MAJOR_VERSION') === true && SODIUM_LIBRARY_MAJOR_VERSION >= 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the binary secret key, optionally derived from the password
|
||||
* or randomly generated
|
||||
*
|
||||
* @param string|null $salt Salt for password-based key derivation
|
||||
* @param array|null $limits Processing limits for password-based key derivation
|
||||
*/
|
||||
public function secretKey(
|
||||
#[SensitiveParameter]
|
||||
string|null $salt = null,
|
||||
array|null $limits = null
|
||||
): string {
|
||||
if (isset($this->secretKey) === true) {
|
||||
return $this->secretKey;
|
||||
}
|
||||
|
||||
// derive from password
|
||||
if (isset($this->password) === true) {
|
||||
if ($salt === null || $limits === null) {
|
||||
throw new InvalidArgumentException('Salt and limits are required when deriving a secret key from a password');
|
||||
}
|
||||
|
||||
// access from cache
|
||||
$options = $salt . ':' . implode(',', $limits);
|
||||
if (isset($this->secretKeysByOptions[$options]) === true) {
|
||||
return $this->secretKeysByOptions[$options];
|
||||
}
|
||||
|
||||
return $this->secretKeysByOptions[$options] = sodium_crypto_pwhash(
|
||||
SODIUM_CRYPTO_SECRETBOX_KEYBYTES,
|
||||
$this->password,
|
||||
$salt,
|
||||
$limits[0],
|
||||
$limits[1],
|
||||
SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13
|
||||
);
|
||||
}
|
||||
|
||||
// generate a random key
|
||||
return $this->secretKey = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes a variable from memory if it is a string
|
||||
*/
|
||||
protected function memzero(mixed &$value): void
|
||||
{
|
||||
if (is_string($value) === true) {
|
||||
sodium_memzero($value);
|
||||
$value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Base32\Base32;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use SensitiveParameter;
|
||||
|
||||
/**
|
||||
* The TOTP class handles the generation and verification
|
||||
* of time-based one-time passwords according to RFC6238
|
||||
* with the SHA1 algorithm, 30 second intervals and 6 digits
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Totp
|
||||
{
|
||||
/**
|
||||
* Binary secret
|
||||
*/
|
||||
protected string $secret;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*
|
||||
* @param string|null $secret Existing secret in Base32 format
|
||||
* or `null` to generate a new one
|
||||
* @param bool $force Whether to skip the secret length validation;
|
||||
* WARNING: Only ever set this to `true` when
|
||||
* generating codes for third-party services
|
||||
*/
|
||||
public function __construct(
|
||||
#[SensitiveParameter]
|
||||
string|null $secret = null,
|
||||
bool $force = false
|
||||
) {
|
||||
// if provided, decode the existing secret into binary
|
||||
if ($secret !== null) {
|
||||
$this->secret = Base32::decode($secret);
|
||||
}
|
||||
|
||||
// otherwise generate a new one;
|
||||
// 20 bytes are the length of the SHA1 HMAC
|
||||
$this->secret ??= random_bytes(20);
|
||||
|
||||
// safety check to avoid accidental insecure secrets
|
||||
if ($force === false && strlen($this->secret) !== 20) {
|
||||
throw new InvalidArgumentException('TOTP secrets should be 32 Base32 digits (= 20 bytes)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the current TOTP code
|
||||
*
|
||||
* @param int $offset Optional counter offset to generate
|
||||
* previous or upcoming codes
|
||||
*/
|
||||
public function generate(int $offset = 0): string
|
||||
{
|
||||
// generate a new code every 30 seconds
|
||||
$counter = floor(time() / 30) + $offset;
|
||||
|
||||
// pack the number into a binary 64-bit unsigned int
|
||||
$binaryCounter = pack('J', $counter);
|
||||
|
||||
// on 32-bit systems, we need to pack into a binary 32-bit
|
||||
// unsigned int and prepend 4 null bytes to get a 64-bit value
|
||||
// @codeCoverageIgnoreStart
|
||||
if (PHP_INT_SIZE < 8) {
|
||||
$binaryCounter = "\0\0\0\0" . pack('N', $counter);
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
// create a binary HMAC from the binary counter and the binary secret
|
||||
$binaryHmac = hash_hmac('sha1', $binaryCounter, $this->secret, true);
|
||||
|
||||
// convert the HMAC into an array of byte values (from 0-255)
|
||||
$bytes = unpack('C*', $binaryHmac);
|
||||
|
||||
// perform dynamic truncation to four bytes according to RFC6238 & RFC4226
|
||||
$byteOffset = (end($bytes) & 0xF);
|
||||
$code = (($bytes[$byteOffset + 1] & 0x7F) << 24) |
|
||||
($bytes[$byteOffset + 2] << 16) |
|
||||
($bytes[$byteOffset + 3] << 8) |
|
||||
$bytes[$byteOffset + 4];
|
||||
|
||||
// truncate the resulting number to at max six digits
|
||||
$code %= 1000000;
|
||||
|
||||
// format as a six-digit string, left-padded with zeros
|
||||
return sprintf('%06d', $code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the secret in human-readable Base32 format
|
||||
*/
|
||||
public function secret(): string
|
||||
{
|
||||
return Base32::encode($this->secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a `otpauth://` URI for use in a setup QR code or link
|
||||
*
|
||||
* @param string $issuer Name of the site the code is valid for
|
||||
* @param string $label Account name the code is valid for
|
||||
*/
|
||||
public function uri(string $issuer, string $label): string
|
||||
{
|
||||
$query = http_build_query([
|
||||
'secret' => $this->secret(),
|
||||
'issuer' => $issuer
|
||||
], '', '&', PHP_QUERY_RFC3986);
|
||||
|
||||
return 'otpauth://totp/' . rawurlencode($issuer) .
|
||||
':' . rawurlencode($label) . '?' . $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Securely checks the provided TOTP code against the
|
||||
* current, the direct previous and following codes
|
||||
*/
|
||||
public function verify(string $totp): bool
|
||||
{
|
||||
// strip out any non-numeric character (e.g. spaces)
|
||||
// from user input to increase UX
|
||||
$totp = preg_replace('/[^0-9]/', '', $totp);
|
||||
|
||||
// also allow the previous and upcoming codes
|
||||
// to account for time sync issues
|
||||
foreach ([0, -1, 1] as $offset) {
|
||||
if (hash_equals($this->generate($offset), $totp) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,13 @@ class Tpl
|
||||
/**
|
||||
* Renders the template
|
||||
*
|
||||
* @param string|null $file
|
||||
* @param array $data
|
||||
* @return string
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function load(
|
||||
string|null $file = null,
|
||||
array $data = []
|
||||
): string {
|
||||
public static function load(?string $file = null, array $data = []): string
|
||||
{
|
||||
if ($file === null || is_file($file) === false) {
|
||||
return '';
|
||||
}
|
||||
@@ -41,10 +42,10 @@ class Tpl
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
if ($exception !== null) {
|
||||
throw $exception;
|
||||
if ($exception === null) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
return $content;
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Countable;
|
||||
use Exception;
|
||||
use Kirby\Content\Field;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Http\Idn;
|
||||
use Kirby\Uuid\Uuid;
|
||||
use ReflectionFunction;
|
||||
use Throwable;
|
||||
|
||||
@@ -24,19 +21,23 @@ class V
|
||||
{
|
||||
/**
|
||||
* An array with all installed validators
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static array $validators = [];
|
||||
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,
|
||||
array $messages = []
|
||||
): array {
|
||||
public static function errors($input, array $rules, $messages = []): array
|
||||
{
|
||||
$errors = static::value($input, $rules, $messages, false);
|
||||
|
||||
return $errors === true ? [] : $errors;
|
||||
@@ -46,12 +47,14 @@ class V
|
||||
* Runs a number of validators on a set of data and
|
||||
* checks if the data is invalid
|
||||
* @since 3.7.0
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $rules
|
||||
* @param array $messages
|
||||
* @return array
|
||||
*/
|
||||
public static function invalid(
|
||||
array $data = [],
|
||||
array $rules = [],
|
||||
array $messages = []
|
||||
): array {
|
||||
public static function invalid(array $data = [], array $rules = [], array $messages = []): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $field => $validations) {
|
||||
@@ -113,11 +116,13 @@ class V
|
||||
* 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|null {
|
||||
public static function message(string $validatorName, ...$params): ?string
|
||||
{
|
||||
$validatorName = strtolower($validatorName);
|
||||
$translationKey = 'error.validation.' . $validatorName;
|
||||
$validators = array_change_key_case(static::$validators);
|
||||
@@ -134,13 +139,16 @@ class V
|
||||
$value = $params[$index] ?? null;
|
||||
|
||||
if (is_array($value) === true) {
|
||||
foreach ($value as $key => $item) {
|
||||
if (is_array($item) === true) {
|
||||
$value[$key] = A::implode($item, '|');
|
||||
try {
|
||||
foreach ($value as $key => $item) {
|
||||
if (is_array($item) === true) {
|
||||
$value[$key] = implode('|', $item);
|
||||
}
|
||||
}
|
||||
$value = implode(', ', $value);
|
||||
} catch (Throwable $e) {
|
||||
$value = '-';
|
||||
}
|
||||
|
||||
$value = implode(', ', $value);
|
||||
}
|
||||
|
||||
$arguments[$parameter->getName()] = $value;
|
||||
@@ -151,6 +159,8 @@ class V
|
||||
|
||||
/**
|
||||
* Return the list of all validators
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function validators(): array
|
||||
{
|
||||
@@ -161,13 +171,15 @@ class V
|
||||
* Validate a single value against
|
||||
* a set of rules, using all registered
|
||||
* validators
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array $rules
|
||||
* @param array $messages
|
||||
* @param bool $fail
|
||||
* @return bool|array
|
||||
*/
|
||||
public static function value(
|
||||
$value,
|
||||
array $rules,
|
||||
array $messages = [],
|
||||
bool $fail = true
|
||||
): bool|array {
|
||||
public static function value($value, array $rules, array $messages = [], bool $fail = true)
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $validatorName => $validatorOptions) {
|
||||
@@ -199,6 +211,10 @@ class V
|
||||
* Validate an input array against
|
||||
* a set of rules, using all registered
|
||||
* validators
|
||||
*
|
||||
* @param array $input
|
||||
* @param array $rules
|
||||
* @return bool
|
||||
*/
|
||||
public static function input(array $input, array $rules): bool
|
||||
{
|
||||
@@ -233,6 +249,10 @@ class V
|
||||
|
||||
/**
|
||||
* Calls an installed validator and passes all arguments
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return bool
|
||||
*/
|
||||
public static function __callStatic(string $method, array $arguments): bool
|
||||
{
|
||||
@@ -278,17 +298,8 @@ V::$validators = [
|
||||
* 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 with the callback sent by the user
|
||||
* It's ideal for one-time custom validations
|
||||
*/
|
||||
'callback' => function ($value, callable $callback): bool {
|
||||
return $callback($value);
|
||||
return V::min($value, $min) === true &&
|
||||
V::max($value, $max) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -306,7 +317,7 @@ V::$validators = [
|
||||
* Pass an operator as second argument and another date as
|
||||
* third argument to compare them.
|
||||
*/
|
||||
'date' => function (string|null $value, string $operator = null, string $test = null): bool {
|
||||
'date' => function (?string $value, string $operator = null, string $test = null): bool {
|
||||
// make sure $value is a string
|
||||
$value ??= '';
|
||||
|
||||
@@ -327,16 +338,22 @@ V::$validators = [
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($operator) {
|
||||
'!=' => $value !== $test,
|
||||
'<' => $value < $test,
|
||||
'>' => $value > $test,
|
||||
'<=' => $value <= $test,
|
||||
'>=' => $value >= $test,
|
||||
'==' => $value === $test,
|
||||
switch ($operator) {
|
||||
case '!=':
|
||||
return $value !== $test;
|
||||
case '<':
|
||||
return $value < $test;
|
||||
case '>':
|
||||
return $value > $test;
|
||||
case '<=':
|
||||
return $value <= $test;
|
||||
case '>=':
|
||||
return $value >= $test;
|
||||
case '==':
|
||||
return $value === $test;
|
||||
}
|
||||
|
||||
default => throw new InvalidArgumentException('Invalid date comparison operator: "' . $operator . '". Allowed operators: "==", "!=", "<", "<=", ">", ">="')
|
||||
};
|
||||
throw new InvalidArgumentException('Invalid date comparison operator: "' . $operator . '". Allowed operators: "==", "!=", "<", "<=", ">", ">="');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -363,7 +380,7 @@ V::$validators = [
|
||||
if (filter_var($value, FILTER_VALIDATE_EMAIL) === false) {
|
||||
try {
|
||||
$email = Idn::encodeEmail($value);
|
||||
} catch (Throwable) {
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -401,9 +418,8 @@ V::$validators = [
|
||||
* Checks for a valid filename
|
||||
*/
|
||||
'filename' => function ($value): bool {
|
||||
return
|
||||
V::match($value, '/^[a-z0-9@._-]+$/i') === true &&
|
||||
V::min($value, 2) === true;
|
||||
return V::match($value, '/^[a-z0-9@._-]+$/i') === true &&
|
||||
V::min($value, 2) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -454,7 +470,7 @@ V::$validators = [
|
||||
* Checks if the value matches the given regular expression
|
||||
*/
|
||||
'match' => function ($value, string $pattern): bool {
|
||||
return preg_match($pattern, (string)$value) === 1;
|
||||
return preg_match($pattern, $value) !== 0;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -563,7 +579,7 @@ V::$validators = [
|
||||
'size' => function ($value, $size, $operator = '=='): bool {
|
||||
// if value is field object, first convert it to a readable value
|
||||
// it is important to check at the beginning as the value can be string or numeric
|
||||
if ($value instanceof Field) {
|
||||
if (is_a($value, '\Kirby\Cms\Field') === true) {
|
||||
$value = $value->value();
|
||||
}
|
||||
|
||||
@@ -574,7 +590,7 @@ V::$validators = [
|
||||
} elseif (is_array($value) === true) {
|
||||
$count = count($value);
|
||||
} elseif (is_object($value) === true) {
|
||||
if ($value instanceof Countable) {
|
||||
if ($value instanceof \Countable) {
|
||||
$count = count($value);
|
||||
} elseif (method_exists($value, 'count') === true) {
|
||||
$count = $value->count();
|
||||
@@ -585,13 +601,18 @@ V::$validators = [
|
||||
throw new Exception('$value is of type without size');
|
||||
}
|
||||
|
||||
return match ($operator) {
|
||||
'<' => $count < $size,
|
||||
'>' => $count > $size,
|
||||
'<=' => $count <= $size,
|
||||
'>=' => $count >= $size,
|
||||
default => $count == $size
|
||||
};
|
||||
switch ($operator) {
|
||||
case '<':
|
||||
return $count < $size;
|
||||
case '>':
|
||||
return $count > $size;
|
||||
case '<=':
|
||||
return $count <= $size;
|
||||
case '>=':
|
||||
return $count >= $size;
|
||||
default:
|
||||
return $count == $size;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -601,13 +622,6 @@ V::$validators = [
|
||||
return Str::startsWith($value, $start);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid unformatted telephone number
|
||||
*/
|
||||
'tel' => function ($value): bool {
|
||||
return V::match($value, '!^[+]{0,1}[0-9]+$!');
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for valid time
|
||||
*/
|
||||
@@ -623,12 +637,5 @@ V::$validators = [
|
||||
// Added localhost support and removed 127.*.*.* ip restriction
|
||||
$regex = '_^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\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]))|(?:localhost)|(?:(?:[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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid Uuid, optionally for specific model type
|
||||
*/
|
||||
'uuid' => function (string $value, string $type = null): bool {
|
||||
return Uuid::is($value, $type);
|
||||
}
|
||||
];
|
||||
|
||||
@@ -18,17 +18,36 @@ use Throwable;
|
||||
class View
|
||||
{
|
||||
/**
|
||||
* Creates a new view object
|
||||
* The absolute path to the view file
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public function __construct(
|
||||
// The absolute path to the view file
|
||||
protected string $file,
|
||||
protected array $data = []
|
||||
) {
|
||||
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
|
||||
* Returns the view's data array
|
||||
* without globals.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
@@ -37,6 +56,8 @@ class View
|
||||
|
||||
/**
|
||||
* Checks if the template file exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
@@ -45,14 +66,18 @@ class View
|
||||
|
||||
/**
|
||||
* Returns the view file
|
||||
*
|
||||
* @return string|false
|
||||
*/
|
||||
public function file(): string
|
||||
public function file()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an error message for the missing view exception
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function missingViewMessage(): string
|
||||
{
|
||||
@@ -61,6 +86,8 @@ class View
|
||||
|
||||
/**
|
||||
* Renders the view
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render(): string
|
||||
{
|
||||
@@ -70,6 +97,7 @@ class View
|
||||
|
||||
ob_start();
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
F::load($this->file(), null, $this->data());
|
||||
} catch (Throwable $e) {
|
||||
@@ -79,15 +107,17 @@ class View
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
if (($exception ?? null) !== null) {
|
||||
throw $exception;
|
||||
if ($exception === null) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
return $content;
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see ::render()
|
||||
* Alias for View::render()
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toString(): string
|
||||
{
|
||||
@@ -98,7 +128,7 @@ class View
|
||||
* Magic string converter to enable
|
||||
* converting view objects to string
|
||||
*
|
||||
* @see ::render()
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Kirby\Cms\Helpers;
|
||||
use SimpleXMLElement;
|
||||
|
||||
/**
|
||||
@@ -18,8 +17,10 @@ class Xml
|
||||
{
|
||||
/**
|
||||
* HTML to XML conversion table for entities
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static array|null $entities = [
|
||||
public static $entities = [
|
||||
' ' => ' ', '¡' => '¡', '¢' => '¢', '£' => '£', '¤' => '¤', '¥' => '¥', '¦' => '¦', '§' => '§',
|
||||
'¨' => '¨', '©' => '©', 'ª' => 'ª', '«' => '«', '¬' => '¬', '­' => '­', '®' => '®', '¯' => '¯',
|
||||
'°' => '°', '±' => '±', '²' => '²', '³' => '³', '´' => '´', 'µ' => 'µ', '¶' => '¶', '·' => '·',
|
||||
@@ -70,10 +71,8 @@ class Xml
|
||||
* If used with a `$name` array, this can be set to `false` to disable attribute sorting.
|
||||
* @return string|null The generated XML attributes string
|
||||
*/
|
||||
public static function attr(
|
||||
string|array $name,
|
||||
$value = null
|
||||
): string|null {
|
||||
public static function attr($name, $value = null): ?string
|
||||
{
|
||||
if (is_array($name) === true) {
|
||||
if ($value !== false) {
|
||||
ksort($name);
|
||||
@@ -81,34 +80,26 @@ class Xml
|
||||
|
||||
$attributes = [];
|
||||
foreach ($name as $key => $val) {
|
||||
if (is_int($key) === true) {
|
||||
$key = $val;
|
||||
$val = true;
|
||||
}
|
||||
$a = static::attr($key, $val);
|
||||
|
||||
if ($attribute = static::attr($key, $val)) {
|
||||
$attributes[] = $attribute;
|
||||
if ($a) {
|
||||
$attributes[] = $a;
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $attributes);
|
||||
}
|
||||
|
||||
if ($value === null || $value === false || $value === []) {
|
||||
if ($value === null || $value === '' || $value === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: In 5.0, remove this block to render space as space
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($value === ' ') {
|
||||
Helpers::deprecated('Passing a single space as value to `Xml::attr()` has been deprecated. In a future version, passing a single space won\'t render an empty value anymore but a single space. To render an empty value, please pass an empty string.', 'xml-attr-single-space');
|
||||
|
||||
return $name . '=""';
|
||||
return strtolower($name) . '=""';
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ($value === true) {
|
||||
return $name . '="' . $name . '"';
|
||||
if (is_bool($value) === true) {
|
||||
return $value === true ? strtolower($name) . '="' . strtolower($name) . '"' : null;
|
||||
}
|
||||
|
||||
if (is_array($value) === true) {
|
||||
@@ -124,7 +115,7 @@ class Xml
|
||||
$value = static::encode($value);
|
||||
}
|
||||
|
||||
return $name . '="' . $value . '"';
|
||||
return strtolower($name) . '="' . $value . '"';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,13 +133,8 @@ class Xml
|
||||
* @param int $level The indentation level (used internally)
|
||||
* @return string The XML string
|
||||
*/
|
||||
public static function create(
|
||||
array|string $props,
|
||||
string $name = 'root',
|
||||
bool $head = true,
|
||||
string $indent = ' ',
|
||||
int $level = 0
|
||||
): string {
|
||||
public static function create($props, string $name = 'root', bool $head = true, string $indent = ' ', int $level = 0): string
|
||||
{
|
||||
if (is_array($props) === true) {
|
||||
if (A::isAssociative($props) === true) {
|
||||
// a tag with attributes or named children
|
||||
@@ -191,14 +177,14 @@ class Xml
|
||||
} else {
|
||||
// scalar value
|
||||
|
||||
$result = static::tag($name, $props, [], $indent, $level);
|
||||
$result = static::tag($name, $props, null, $indent, $level);
|
||||
}
|
||||
|
||||
if ($head === true) {
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>' . PHP_EOL . $result;
|
||||
} else {
|
||||
return $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,10 +194,17 @@ class Xml
|
||||
* echo Xml::decode('some über <em>crazy</em> stuff');
|
||||
* // output: some über crazy stuff
|
||||
* ```
|
||||
*
|
||||
* @param string|null $string
|
||||
* @return string
|
||||
*/
|
||||
public static function decode(string|null $string): string
|
||||
public static function decode(?string $string): string
|
||||
{
|
||||
$string = strip_tags($string ?? '');
|
||||
if ($string === null) {
|
||||
$string = '';
|
||||
}
|
||||
|
||||
$string = strip_tags($string);
|
||||
return html_entity_decode($string, ENT_COMPAT, 'utf-8');
|
||||
}
|
||||
|
||||
@@ -226,12 +219,12 @@ class Xml
|
||||
* // output: some über crazy stuff
|
||||
* ```
|
||||
*
|
||||
* @param string|null $string
|
||||
* @param bool $html True = Convert to HTML-safe first
|
||||
* @return string
|
||||
*/
|
||||
public static function encode(
|
||||
string|null $string,
|
||||
bool $html = true
|
||||
): string {
|
||||
public static function encode(?string $string, bool $html = true): string
|
||||
{
|
||||
if ($string === null) {
|
||||
return '';
|
||||
}
|
||||
@@ -249,6 +242,8 @@ class Xml
|
||||
|
||||
/**
|
||||
* Returns the HTML-to-XML entity translation table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function entities(): array
|
||||
{
|
||||
@@ -258,12 +253,27 @@ class Xml
|
||||
/**
|
||||
* Parses an XML string and returns an array
|
||||
*
|
||||
* @param string $xml
|
||||
* @return array|null Parsed array or `null` on error
|
||||
*/
|
||||
public static function parse(string $xml): array|null
|
||||
public static function parse(string $xml): ?array
|
||||
{
|
||||
$loaderSetting = null;
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
// prevent loading external entities to protect against XXE attacks;
|
||||
// only needed for PHP versions before 8.0 (the function was deprecated
|
||||
// as the disabled state is the new default in PHP 8.0+)
|
||||
$loaderSetting = libxml_disable_entity_loader(true);
|
||||
}
|
||||
|
||||
$xml = @simplexml_load_string($xml);
|
||||
|
||||
if (\PHP_VERSION_ID < 80000) {
|
||||
// ensure that we don't alter global state by
|
||||
// resetting the original value
|
||||
libxml_disable_entity_loader($loaderSetting);
|
||||
}
|
||||
|
||||
if (is_object($xml) !== true) {
|
||||
return null;
|
||||
}
|
||||
@@ -275,12 +285,12 @@ class Xml
|
||||
* Breaks a SimpleXMLElement down into a simpler tree
|
||||
* structure of arrays and strings
|
||||
*
|
||||
* @param \SimpleXMLElement $element
|
||||
* @param bool $collectName Whether the element name should be collected (for the root element)
|
||||
* @return array|string
|
||||
*/
|
||||
public static function simplify(
|
||||
SimpleXMLElement $element,
|
||||
bool $collectName = true
|
||||
): array|string {
|
||||
public static function simplify(SimpleXMLElement $element, bool $collectName = true)
|
||||
{
|
||||
// get all XML namespaces of the whole document to iterate over later;
|
||||
// we don't need the global namespace (empty string) in the list
|
||||
$usedNamespaces = $element->getNamespaces(true);
|
||||
@@ -345,17 +355,18 @@ class Xml
|
||||
}
|
||||
|
||||
return $array;
|
||||
} else {
|
||||
// we didn't find any XML children above, only use the string value
|
||||
$element = (string)$element;
|
||||
|
||||
if (count($array) > 0) {
|
||||
$array['@value'] = $element;
|
||||
|
||||
return $array;
|
||||
} else {
|
||||
return $element;
|
||||
}
|
||||
}
|
||||
|
||||
// we didn't find any XML children above, only use the string value
|
||||
$element = (string)$element;
|
||||
|
||||
if (count($array) === 0) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
$array['@value'] = $element;
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -369,13 +380,8 @@ class Xml
|
||||
* @param int $level Indentation level
|
||||
* @return string The generated XML
|
||||
*/
|
||||
public static function tag(
|
||||
string $name,
|
||||
array|string|null $content = '',
|
||||
array $attr = [],
|
||||
string $indent = null,
|
||||
int $level = 0
|
||||
): string {
|
||||
public static function tag(string $name, $content = '', array $attr = null, ?string $indent = null, int $level = 0): string
|
||||
{
|
||||
$attr = static::attr($attr);
|
||||
$start = '<' . $name . ($attr ? ' ' . $attr : '') . '>';
|
||||
$startShort = '<' . $name . ($attr ? ' ' . $attr : '') . static::$void;
|
||||
@@ -403,8 +409,11 @@ class Xml
|
||||
|
||||
/**
|
||||
* Properly encodes tag contents
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string|null
|
||||
*/
|
||||
public static function value($value): string|null
|
||||
public static function value($value): ?string
|
||||
{
|
||||
if ($value === true) {
|
||||
return 'true';
|
||||
|
||||
Reference in New Issue
Block a user