Upgrade to 4.0.0
This commit is contained in:
@@ -37,7 +37,7 @@ class A
|
||||
*
|
||||
* @param mixed ...$args Parameters to pass to the closures
|
||||
*/
|
||||
public static function apply(array $array, ...$args): array
|
||||
public static function apply(array $array, mixed ...$args): array
|
||||
{
|
||||
array_walk_recursive($array, function (&$item) use ($args) {
|
||||
if ($item instanceof Closure) {
|
||||
@@ -49,16 +49,52 @@ class A
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of elements in an array
|
||||
* Returns the average value of an array
|
||||
*
|
||||
* @param array $array
|
||||
* @return int
|
||||
* @param array $array The source array
|
||||
* @param int $decimals The number of decimals to return
|
||||
* @return float|null The average value
|
||||
*/
|
||||
public static function average(array $array, int $decimals = 0): float|null
|
||||
{
|
||||
if (empty($array) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round((array_sum($array) / sizeof($array)), $decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of elements in an array
|
||||
*/
|
||||
public static function count(array $array): int
|
||||
{
|
||||
return count($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges arrays recursively
|
||||
*
|
||||
* <code>
|
||||
* $defaults = [
|
||||
* 'username' => 'admin',
|
||||
* 'password' => 'admin',
|
||||
* ];
|
||||
*
|
||||
* $options = A::extend($defaults, ['password' => 'super-secret']);
|
||||
* // returns: [
|
||||
* // 'username' => 'admin',
|
||||
* // 'password' => 'super-secret'
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @psalm-suppress NamedArgumentNotAllowed
|
||||
*/
|
||||
public static function extend(array ...$arrays): array
|
||||
{
|
||||
return array_merge_recursive(...$arrays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if every element in the array passes the test
|
||||
*
|
||||
@@ -75,9 +111,7 @@ class A
|
||||
* </code>
|
||||
*
|
||||
* @since 3.9.8
|
||||
* @param array $array
|
||||
* @param callable(mixed $value, int|string $key, array $array):bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public static function every(array $array, callable $test): bool
|
||||
{
|
||||
@@ -90,6 +124,57 @@ class A
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an array up with additional elements to certain amount.
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $result = A::fill($array, 5, 'elephant');
|
||||
*
|
||||
* // result: [
|
||||
* // 'cat',
|
||||
* // 'dog',
|
||||
* // 'bird',
|
||||
* // 'elephant',
|
||||
* // 'elephant',
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param int $limit The number of elements the array should
|
||||
* contain after filling it up.
|
||||
* @param mixed $fill The element, which should be used to
|
||||
* fill the array. If it's a callable, it
|
||||
* will be called with the current index
|
||||
* @return array The filled-up result array
|
||||
*/
|
||||
public static function fill(
|
||||
array $array,
|
||||
int $limit,
|
||||
mixed $fill = 'placeholder'
|
||||
): array {
|
||||
for ($x = count($array); $x < $limit; $x++) {
|
||||
$array[] = is_callable($fill) ? $fill($x) : $fill;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the array using the given callback
|
||||
* using both value and key
|
||||
* @since 3.6.5
|
||||
*/
|
||||
public static function filter(array $array, callable $callback): array
|
||||
{
|
||||
return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first element matching the given callback
|
||||
*
|
||||
@@ -115,9 +200,7 @@ class A
|
||||
* </code>
|
||||
*
|
||||
* @since 3.9.8
|
||||
* @param array $array
|
||||
* @param callable(mixed $value, int|string $key, array $array):bool $callback
|
||||
* @return mixed
|
||||
*/
|
||||
public static function find(array $array, callable $callback): mixed
|
||||
{
|
||||
@@ -130,6 +213,28 @@ class A
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element of an array
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $first = A::first($array);
|
||||
* // first: 'miao'
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return mixed The first element
|
||||
*/
|
||||
public static function first(array $array): mixed
|
||||
{
|
||||
return array_shift($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an element of an array by key
|
||||
*
|
||||
@@ -159,7 +264,7 @@ class A
|
||||
public static function get(
|
||||
$array,
|
||||
string|int|array|null $key,
|
||||
$default = null
|
||||
mixed $default = null
|
||||
) {
|
||||
if (is_array($array) === false) {
|
||||
return $array;
|
||||
@@ -237,22 +342,45 @@ class A
|
||||
|
||||
/**
|
||||
* Checks if array has a value
|
||||
*
|
||||
* @param array $array
|
||||
* @param mixed $value
|
||||
* @param bool $strict
|
||||
* @return bool
|
||||
*/
|
||||
public static function has(array $array, $value, bool $strict = false): bool
|
||||
{
|
||||
public static function has(
|
||||
array $array,
|
||||
mixed $value,
|
||||
bool $strict = false
|
||||
): bool {
|
||||
return in_array($value, $array, $strict);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an array is associative or not
|
||||
*
|
||||
* <code>
|
||||
* $array = ['a', 'b', 'c'];
|
||||
*
|
||||
* A::isAssociative($array);
|
||||
* // returns: false
|
||||
*
|
||||
* $array = ['a' => 'a', 'b' => 'b', 'c' => 'c'];
|
||||
*
|
||||
* A::isAssociative($array);
|
||||
* // returns: true
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The array to analyze
|
||||
* @return bool true: The array is associative false: It's not
|
||||
*/
|
||||
public static function isAssociative(array $array): bool
|
||||
{
|
||||
return ctype_digit(implode('', array_keys($array))) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the elements of an array to a string
|
||||
*/
|
||||
public static function join(array|string $value, string $separator = ', '): string
|
||||
{
|
||||
public static function join(
|
||||
array|string $value,
|
||||
string $separator = ', '
|
||||
): string {
|
||||
if (is_string($value) === true) {
|
||||
return $value;
|
||||
}
|
||||
@@ -271,10 +399,6 @@ class A
|
||||
*
|
||||
* // Now you can access the array by the id
|
||||
* </code>
|
||||
*
|
||||
* @param array $array
|
||||
* @param string|callable $keyBy
|
||||
* @return array
|
||||
*/
|
||||
public static function keyBy(array $array, string|callable $keyBy): array
|
||||
{
|
||||
@@ -290,6 +414,38 @@ class A
|
||||
return array_combine($keys, $array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last element of an array
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $last = A::last($array);
|
||||
* // last: 'tweet'
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return mixed The last element
|
||||
*/
|
||||
public static function last(array $array): mixed
|
||||
{
|
||||
return array_pop($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple wrapper around array_map
|
||||
* with a sane argument order
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public static function map(array $array, callable $map): array
|
||||
{
|
||||
return array_map($map, $array);
|
||||
}
|
||||
|
||||
public const MERGE_OVERWRITE = 0;
|
||||
public const MERGE_APPEND = 1;
|
||||
public const MERGE_REPLACE = 2;
|
||||
@@ -412,246 +568,13 @@ class A
|
||||
|
||||
/**
|
||||
* Reduce an array to a single value
|
||||
*
|
||||
* @param array $array
|
||||
* @param callable $callback
|
||||
* @param mixed $initial
|
||||
* @return mixed
|
||||
*/
|
||||
public static function reduce(array $array, callable $callback, $initial = null): mixed
|
||||
{
|
||||
return array_reduce($array, $callback, $initial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles an array and keeps the keys
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $shuffled = A::shuffle($array);
|
||||
* // output: [
|
||||
* // 'dog' => 'wuff',
|
||||
* // 'cat' => 'miao',
|
||||
* // 'bird' => 'tweet'
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return array The shuffled result array
|
||||
*/
|
||||
public static function shuffle(array $array): array
|
||||
{
|
||||
$keys = array_keys($array);
|
||||
$new = [];
|
||||
|
||||
shuffle($keys);
|
||||
|
||||
// resort the array
|
||||
foreach ($keys as $key) {
|
||||
$new[$key] = $array[$key];
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a slice of an array
|
||||
*
|
||||
* @param array $array
|
||||
* @param int $offset
|
||||
* @param int|null $length
|
||||
* @param bool $preserveKeys
|
||||
* @return array
|
||||
*/
|
||||
public static function slice(
|
||||
public static function reduce(
|
||||
array $array,
|
||||
int $offset,
|
||||
int $length = null,
|
||||
bool $preserveKeys = false
|
||||
): array {
|
||||
return array_slice($array, $offset, $length, $preserveKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if at least one element in the array passes the test
|
||||
*
|
||||
* <code>
|
||||
* $array = [1, 30, 39, 29, 10, 'foo' => 12, 13];
|
||||
*
|
||||
* $isAboveThreshold = fn($value) => $value > 30;
|
||||
* echo A::some($array, $isAboveThreshold) ? 'true' : 'false';
|
||||
* // output: 'true'
|
||||
*
|
||||
* $isStringKey = fn($value, $key) => is_string($key);
|
||||
* echo A::some($array, $isStringKey) ? 'true' : 'false';
|
||||
* // output: 'true'
|
||||
* </code>
|
||||
*
|
||||
* @since 3.9.8
|
||||
* @param array $array
|
||||
* @param callable(mixed $value, int|string $key, array $array):bool $test
|
||||
* @return bool
|
||||
*/
|
||||
public static function some(array $array, callable $test): bool
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
if ($test($value, $key, $array)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums an array
|
||||
*
|
||||
* @param array $array
|
||||
* @return int|float
|
||||
*/
|
||||
public static function sum(array $array): int|float
|
||||
{
|
||||
return array_sum($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element of an array
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $first = A::first($array);
|
||||
* // first: 'miao'
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return mixed The first element
|
||||
*/
|
||||
public static function first(array $array)
|
||||
{
|
||||
return array_shift($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last element of an array
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $last = A::last($array);
|
||||
* // last: 'tweet'
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return mixed The last element
|
||||
*/
|
||||
public static function last(array $array)
|
||||
{
|
||||
return array_pop($array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number of random elements from an array,
|
||||
* either in original or shuffled order
|
||||
*/
|
||||
public static function random(array $array, int $count = 1, bool $shuffle = false): array
|
||||
{
|
||||
if ($shuffle) {
|
||||
return array_slice(self::shuffle($array), 0, $count);
|
||||
}
|
||||
|
||||
if ($count === 1) {
|
||||
$key = array_rand($array);
|
||||
return [$key => $array[$key]];
|
||||
}
|
||||
|
||||
return self::get($array, array_rand($array, $count));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills an array up with additional elements to certain amount.
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $result = A::fill($array, 5, 'elephant');
|
||||
*
|
||||
* // result: [
|
||||
* // 'cat',
|
||||
* // 'dog',
|
||||
* // 'bird',
|
||||
* // 'elephant',
|
||||
* // 'elephant',
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param int $limit The number of elements the array should
|
||||
* contain after filling it up.
|
||||
* @param mixed $fill The element, which should be used to
|
||||
* fill the array. If it's a callable, it
|
||||
* will be called with the current index
|
||||
* @return array The filled-up result array
|
||||
*/
|
||||
public static function fill(array $array, int $limit, $fill = 'placeholder'): array
|
||||
{
|
||||
for ($x = count($array); $x < $limit; $x++) {
|
||||
$array[] = is_callable($fill) ? $fill($x) : $fill;
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple wrapper around array_map
|
||||
* with a sane argument order
|
||||
* @since 3.6.0
|
||||
*/
|
||||
public static function map(array $array, callable $map): array
|
||||
{
|
||||
return array_map($map, $array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an array item to a new index
|
||||
*/
|
||||
public static function move(array $array, int $from, int $to): array
|
||||
{
|
||||
$total = count($array);
|
||||
|
||||
if ($from >= $total || $from < 0) {
|
||||
throw new Exception('Invalid "from" index');
|
||||
}
|
||||
|
||||
if ($to >= $total || $to < 0) {
|
||||
throw new Exception('Invalid "to" index');
|
||||
}
|
||||
|
||||
// remove the item from the array
|
||||
$item = array_splice($array, $from, 1);
|
||||
|
||||
// inject it at the new position
|
||||
array_splice($array, $to, 0, $item);
|
||||
|
||||
return $array;
|
||||
callable $callback,
|
||||
$initial = null
|
||||
): mixed {
|
||||
return array_reduce($array, $callback, $initial);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -685,6 +608,31 @@ class A
|
||||
return array_values(array_diff($required, array_keys($array)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an array item to a new index
|
||||
*/
|
||||
public static function move(array $array, int $from, int $to): array
|
||||
{
|
||||
$total = count($array);
|
||||
|
||||
if ($from >= $total || $from < 0) {
|
||||
throw new Exception('Invalid "from" index');
|
||||
}
|
||||
|
||||
if ($to >= $total || $to < 0) {
|
||||
throw new Exception('Invalid "to" index');
|
||||
}
|
||||
|
||||
// remove the item from the array
|
||||
$item = array_splice($array, $from, 1);
|
||||
|
||||
// inject it at the new position
|
||||
array_splice($array, $to, 0, $item);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes an array into a nested form by converting
|
||||
* dot notation in keys to nested structures
|
||||
@@ -704,7 +652,7 @@ class A
|
||||
|
||||
foreach ($array as $fullKey => $value) {
|
||||
// extract the first part of a multi-level key, keep the others
|
||||
$subKeys = explode('.', $fullKey);
|
||||
$subKeys = is_int($fullKey) ? [$fullKey] : explode('.', $fullKey);
|
||||
$key = array_shift($subKeys);
|
||||
|
||||
// skip the magic for ignored keys
|
||||
@@ -762,6 +710,105 @@ class A
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a number of random elements from an array,
|
||||
* either in original or shuffled order
|
||||
*/
|
||||
public static function random(
|
||||
array $array,
|
||||
int $count = 1,
|
||||
bool $shuffle = false
|
||||
): array {
|
||||
if ($shuffle === true) {
|
||||
return array_slice(self::shuffle($array), 0, $count);
|
||||
}
|
||||
|
||||
if ($count === 1) {
|
||||
$key = array_rand($array);
|
||||
return [$key => $array[$key]];
|
||||
}
|
||||
|
||||
return self::get($array, array_rand($array, $count));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffles an array and keeps the keys
|
||||
*
|
||||
* <code>
|
||||
* $array = [
|
||||
* 'cat' => 'miao',
|
||||
* 'dog' => 'wuff',
|
||||
* 'bird' => 'tweet'
|
||||
* ];
|
||||
*
|
||||
* $shuffled = A::shuffle($array);
|
||||
* // output: [
|
||||
* // 'dog' => 'wuff',
|
||||
* // 'cat' => 'miao',
|
||||
* // 'bird' => 'tweet'
|
||||
* // ];
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @return array The shuffled result array
|
||||
*/
|
||||
public static function shuffle(array $array): array
|
||||
{
|
||||
$keys = array_keys($array);
|
||||
$new = [];
|
||||
|
||||
shuffle($keys);
|
||||
|
||||
// resort the array
|
||||
foreach ($keys as $key) {
|
||||
$new[$key] = $array[$key];
|
||||
}
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a slice of an array
|
||||
*/
|
||||
public static function slice(
|
||||
array $array,
|
||||
int $offset,
|
||||
int|null $length = null,
|
||||
bool $preserveKeys = false
|
||||
): array {
|
||||
return array_slice($array, $offset, $length, $preserveKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if at least one element in the array passes the test
|
||||
*
|
||||
* <code>
|
||||
* $array = [1, 30, 39, 29, 10, 'foo' => 12, 13];
|
||||
*
|
||||
* $isAboveThreshold = fn($value) => $value > 30;
|
||||
* echo A::some($array, $isAboveThreshold) ? 'true' : 'false';
|
||||
* // output: 'true'
|
||||
*
|
||||
* $isStringKey = fn($value, $key) => is_string($key);
|
||||
* echo A::some($array, $isStringKey) ? 'true' : 'false';
|
||||
* // output: 'true'
|
||||
* </code>
|
||||
*
|
||||
* @since 3.9.8
|
||||
* @param callable(mixed $value, int|string $key, array $array):bool $test
|
||||
*/
|
||||
public static function some(array $array, callable $test): bool
|
||||
{
|
||||
foreach ($array as $key => $value) {
|
||||
if ($test($value, $key, $array)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts a multi-dimensional array by a certain column
|
||||
*
|
||||
@@ -815,7 +862,7 @@ class A
|
||||
array $array,
|
||||
string $field,
|
||||
string $direction = 'desc',
|
||||
$method = SORT_REGULAR
|
||||
int $method = SORT_REGULAR
|
||||
): array {
|
||||
$direction = strtolower($direction) === 'desc' ? SORT_DESC : SORT_ASC;
|
||||
$helper = [];
|
||||
@@ -842,63 +889,11 @@ class A
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an array is associative or not
|
||||
*
|
||||
* <code>
|
||||
* $array = ['a', 'b', 'c'];
|
||||
*
|
||||
* A::isAssociative($array);
|
||||
* // returns: false
|
||||
*
|
||||
* $array = ['a' => 'a', 'b' => 'b', 'c' => 'c'];
|
||||
*
|
||||
* A::isAssociative($array);
|
||||
* // returns: true
|
||||
* </code>
|
||||
*
|
||||
* @param array $array The array to analyze
|
||||
* @return bool true: The array is associative false: It's not
|
||||
* Sums an array
|
||||
*/
|
||||
public static function isAssociative(array $array): bool
|
||||
public static function sum(array $array): int|float
|
||||
{
|
||||
return ctype_digit(implode('', array_keys($array))) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the average value of an array
|
||||
*
|
||||
* @param array $array The source array
|
||||
* @param int $decimals The number of decimals to return
|
||||
* @return float The average value
|
||||
*/
|
||||
public static function average(array $array, int $decimals = 0): float|null
|
||||
{
|
||||
if (empty($array) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return round((array_sum($array) / sizeof($array)), $decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges arrays recursively
|
||||
*
|
||||
* <code>
|
||||
* $defaults = [
|
||||
* 'username' => 'admin',
|
||||
* 'password' => 'admin',
|
||||
* ];
|
||||
*
|
||||
* $options = A::extend($defaults, ['password' => 'super-secret']);
|
||||
* // returns: [
|
||||
* // 'username' => 'admin',
|
||||
* // 'password' => 'super-secret'
|
||||
* // ];
|
||||
* </code>
|
||||
*/
|
||||
public static function extend(array ...$arrays): array
|
||||
{
|
||||
return array_merge_recursive(...$arrays);
|
||||
return array_sum($array);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -938,6 +933,22 @@ class A
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove key(s) from an array
|
||||
* @since 3.6.5
|
||||
*/
|
||||
public static function without(array $array, int|string|array $keys): array
|
||||
{
|
||||
if (is_int($keys) === true || is_string($keys) === true) {
|
||||
$keys = static::wrap($keys);
|
||||
}
|
||||
|
||||
return static::filter(
|
||||
$array,
|
||||
fn ($value, $key) => in_array($key, $keys, true) === false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given value in an array
|
||||
* if it's not an array yet.
|
||||
@@ -954,30 +965,4 @@ class A
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the array using the given callback
|
||||
* using both value and key
|
||||
* @since 3.6.5
|
||||
*/
|
||||
public static function filter(array $array, callable $callback): array
|
||||
{
|
||||
return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove key(s) from an array
|
||||
* @since 3.6.5
|
||||
*/
|
||||
public static function without(array $array, int|string|array $keys): array
|
||||
{
|
||||
if (is_int($keys) || is_string($keys)) {
|
||||
$keys = static::wrap($keys);
|
||||
}
|
||||
|
||||
return static::filter(
|
||||
$array,
|
||||
fn ($value, $key) => in_array($key, $keys, true) === false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -30,6 +30,9 @@ 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;
|
||||
@@ -67,8 +70,7 @@ class Collection extends Iterator implements Countable
|
||||
|
||||
/**
|
||||
* Improve var_dump() output
|
||||
*
|
||||
* @return array
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
@@ -521,21 +523,24 @@ 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 $i = true)
|
||||
public function group($field, bool $caseInsensitive = true)
|
||||
{
|
||||
// group by field name
|
||||
if (is_string($field) === true) {
|
||||
return $this->group(function ($item) use ($field, $i) {
|
||||
return $this->group(function ($item) use ($field, $caseInsensitive) {
|
||||
$value = $this->getAttribute($item, $field);
|
||||
|
||||
// ignore upper/lowercase for group names
|
||||
return $i === true ? Str::lower($value) : (string)$value;
|
||||
if ($caseInsensitive === true) {
|
||||
return Str::lower($value);
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -740,14 +745,17 @@ class Collection extends Iterator implements Countable
|
||||
* Add pagination
|
||||
*
|
||||
* @param array ...$arguments
|
||||
* @return static a sliced set of data
|
||||
* @return $this|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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -27,80 +27,44 @@ class Component
|
||||
{
|
||||
/**
|
||||
* Registry for all component mixins
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $mixins = [];
|
||||
public static array $mixins = [];
|
||||
|
||||
/**
|
||||
* Registry for all component types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $types = [];
|
||||
public static array $types = [];
|
||||
|
||||
/**
|
||||
* An array of all passed attributes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attrs = [];
|
||||
protected array $attrs = [];
|
||||
|
||||
/**
|
||||
* An array of all computed properties
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $computed = [];
|
||||
protected array $computed = [];
|
||||
|
||||
/**
|
||||
* An array of all registered methods
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $methods = [];
|
||||
protected array $methods = [];
|
||||
|
||||
/**
|
||||
* An array of all component options
|
||||
* from the component definition
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
protected array|string $options = [];
|
||||
|
||||
/**
|
||||
* An array of all resolved props
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $props = [];
|
||||
protected array $props = [];
|
||||
|
||||
/**
|
||||
* The component type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $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;
|
||||
}
|
||||
protected string $type;
|
||||
|
||||
/**
|
||||
* Creates a new component for the given type
|
||||
@@ -133,8 +97,29 @@ 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
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
@@ -167,24 +152,29 @@ class Component
|
||||
*/
|
||||
protected function applyProps(array $props): void
|
||||
{
|
||||
foreach ($props as $propName => $propFunction) {
|
||||
if ($propFunction instanceof Closure) {
|
||||
if (isset($this->attrs[$propName]) === true) {
|
||||
foreach ($props as $name => $function) {
|
||||
if ($function instanceof Closure) {
|
||||
if (isset($this->attrs[$name]) === true) {
|
||||
try {
|
||||
$this->$propName = $this->props[$propName] = $propFunction->call($this, $this->attrs[$propName]);
|
||||
$this->$name = $this->props[$name] = $function->call(
|
||||
$this,
|
||||
$this->attrs[$name]
|
||||
);
|
||||
continue;
|
||||
} catch (TypeError) {
|
||||
throw new TypeError('Invalid value for "' . $propName . '"');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$this->$propName = $this->props[$propName] = $propFunction->call($this);
|
||||
} catch (ArgumentCountError) {
|
||||
throw new ArgumentCountError('Please provide a value for "' . $propName . '"');
|
||||
throw new TypeError('Invalid value for "' . $name . '"');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->$propName = $this->props[$propName] = $propFunction;
|
||||
|
||||
try {
|
||||
$this->$name = $this->props[$name] = $function->call($this);
|
||||
continue;
|
||||
} catch (ArgumentCountError) {
|
||||
throw new ArgumentCountError('Please provide a value for "' . $name . '"');
|
||||
}
|
||||
}
|
||||
|
||||
$this->$name = $this->props[$name] = $function;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,9 +184,9 @@ class Component
|
||||
*/
|
||||
protected function applyComputed(array $computed): void
|
||||
{
|
||||
foreach ($computed as $computedName => $computedFunction) {
|
||||
if ($computedFunction instanceof Closure) {
|
||||
$this->$computedName = $this->computed[$computedName] = $computedFunction->call($this);
|
||||
foreach ($computed as $name => $function) {
|
||||
if ($function instanceof Closure) {
|
||||
$this->$name = $this->computed[$name] = $function->call($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +204,10 @@ 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,
|
||||
allowOutput: false
|
||||
);
|
||||
}
|
||||
|
||||
return $definition;
|
||||
@@ -244,20 +237,21 @@ class Component
|
||||
}
|
||||
|
||||
// inject mixins
|
||||
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
|
||||
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] = F::load(static::$mixins[$mixin], allowOutput: false);
|
||||
}
|
||||
|
||||
$options = array_replace_recursive(
|
||||
static::$mixins[$mixin] = F::load(
|
||||
static::$mixins[$mixin],
|
||||
$options
|
||||
allowOutput: false
|
||||
);
|
||||
}
|
||||
|
||||
$options = array_replace_recursive(
|
||||
static::$mixins[$mixin],
|
||||
$options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,8 +263,10 @@ class Component
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
if (($this->options['toArray'] ?? null) instanceof Closure) {
|
||||
return $this->options['toArray']->call($this);
|
||||
$closure = $this->options['toArray'] ?? null;
|
||||
|
||||
if ($closure instanceof Closure) {
|
||||
return $closure->call($this);
|
||||
}
|
||||
|
||||
$array = array_merge($this->attrs, $this->props, $this->computed);
|
||||
|
@@ -14,8 +14,5 @@ namespace Kirby\Toolkit;
|
||||
*/
|
||||
class Config extends Silo
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public static $data = [];
|
||||
public static array $data = [];
|
||||
}
|
||||
|
@@ -49,7 +49,9 @@ 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);
|
||||
|
@@ -7,6 +7,7 @@ use DateTime;
|
||||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use IntlDateFormatter;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
@@ -119,6 +120,20 @@ class Date extends DateTime
|
||||
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
|
||||
*/
|
||||
@@ -399,7 +414,6 @@ 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
|
||||
|
@@ -169,6 +169,8 @@ class Dom
|
||||
DOMAttr $attr,
|
||||
array $options
|
||||
): bool|string {
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
$allowedTags = $options['allowedTags'];
|
||||
|
||||
// check if the attribute is in the list of global allowed attributes
|
||||
@@ -217,6 +219,8 @@ class Dom
|
||||
DOMAttr $attr,
|
||||
array $options
|
||||
): bool|string {
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
$allowedAttrs = $options['allowedAttrs'];
|
||||
|
||||
if ($allowedAttrs === true) {
|
||||
@@ -255,6 +259,8 @@ class Dom
|
||||
string $url,
|
||||
array $options
|
||||
): bool|string {
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
$url = Str::lower($url);
|
||||
|
||||
// allow empty URL values
|
||||
@@ -274,7 +280,7 @@ class Dom
|
||||
|
||||
// allow site-internal URLs that didn't match the
|
||||
// protocol-relative check above
|
||||
if (mb_substr($url, 0, 1) === '/') {
|
||||
if (mb_substr($url, 0, 1) === '/' && $options['allowHostRelativeUrls'] !== true) {
|
||||
// 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)) {
|
||||
@@ -423,6 +429,8 @@ class Dom
|
||||
array $options,
|
||||
Closure|null $compare = null
|
||||
): string|false {
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
$allowedNamespaces = $options['allowedNamespaces'];
|
||||
$localName = $node->localName;
|
||||
$compare ??= fn ($expected, $real): bool => $expected === $real;
|
||||
@@ -517,6 +525,9 @@ 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`
|
||||
@@ -547,20 +558,7 @@ class Dom
|
||||
*/
|
||||
public function sanitize(array $options): array
|
||||
{
|
||||
$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);
|
||||
$options = static::normalizeSanitizeOptions($options);
|
||||
|
||||
$errors = [];
|
||||
|
||||
@@ -701,6 +699,38 @@ class Dom
|
||||
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;
|
||||
}
|
||||
|
||||
$options = array_merge([
|
||||
'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);
|
||||
|
||||
$options['_normalized'] = true;
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes an attribute
|
||||
*
|
||||
@@ -841,14 +871,14 @@ 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) ?? []);
|
||||
$errors = array_merge($errors, $options['attrCallback']($attr, $options) ?? []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// custom check
|
||||
if ($options['elementCallback']) {
|
||||
$errors = array_merge($errors, $options['elementCallback']($element) ?? []);
|
||||
$errors = array_merge($errors, $options['elementCallback']($element, $options) ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -898,7 +928,7 @@ class Dom
|
||||
}
|
||||
|
||||
if ($options['doctypeCallback']) {
|
||||
$options['doctypeCallback']($doctype);
|
||||
$options['doctypeCallback']($doctype, $options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -323,7 +323,7 @@ class Html extends Xml
|
||||
{
|
||||
$attr = array_merge([
|
||||
'src' => $src,
|
||||
'alt' => ' '
|
||||
'alt' => ''
|
||||
], $attr);
|
||||
|
||||
return static::tag('img', '', $attr);
|
||||
@@ -597,7 +597,7 @@ class Html extends Xml
|
||||
return false;
|
||||
}
|
||||
|
||||
return preg_match('!^[a-zA-Z0-9_-]+$!', $id);
|
||||
return preg_match('!^[a-zA-Z0-9_-]+$!', $id) === 1;
|
||||
};
|
||||
|
||||
switch ($path->toString()) {
|
||||
|
@@ -18,62 +18,51 @@ class I18n
|
||||
{
|
||||
/**
|
||||
* Custom loader function
|
||||
*
|
||||
* @var Closure
|
||||
*/
|
||||
public static $load = null;
|
||||
public static Closure|null $load = null;
|
||||
|
||||
/**
|
||||
* Current locale
|
||||
*
|
||||
* @var string|\Closure
|
||||
*/
|
||||
public static $locale = 'en';
|
||||
public static string|Closure|null $locale = 'en';
|
||||
|
||||
/**
|
||||
* All registered translations
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $translations = [];
|
||||
public static array $translations = [];
|
||||
|
||||
/**
|
||||
* The fallback locale or a
|
||||
* list of fallback locales
|
||||
*
|
||||
* @var string|array|\Closure|null
|
||||
* The fallback locale or a list of fallback locales
|
||||
*/
|
||||
public static $fallback = ['en'];
|
||||
public static string|array|Closure|null $fallback = ['en'];
|
||||
|
||||
/**
|
||||
* Cache of `NumberFormatter` objects by locale
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $decimalsFormatters = [];
|
||||
protected static array $decimalsFormatters = [];
|
||||
|
||||
/**
|
||||
* Returns the list of fallback locales
|
||||
*/
|
||||
public static function fallbacks(): array
|
||||
{
|
||||
if (
|
||||
is_array(static::$fallback) === true ||
|
||||
is_string(static::$fallback) === true
|
||||
) {
|
||||
return A::wrap(static::$fallback);
|
||||
if (is_callable(static::$fallback) === true) {
|
||||
static::$fallback = (static::$fallback)();
|
||||
}
|
||||
|
||||
if (is_callable(static::$fallback) === true) {
|
||||
return static::$fallback = A::wrap((static::$fallback)());
|
||||
if (is_array(static::$fallback) === true) {
|
||||
return static::$fallback;
|
||||
}
|
||||
|
||||
if (is_string(static::$fallback) === true) {
|
||||
return 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 bool $none If true, 'none' will be returned if the count is 0
|
||||
*/
|
||||
@@ -98,88 +87,24 @@ class I18n
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale code
|
||||
* Returns the current locale code
|
||||
*/
|
||||
public static function locale(): string
|
||||
{
|
||||
if (is_string(static::$locale) === true) {
|
||||
return static::$locale;
|
||||
if (is_callable(static::$locale) === true) {
|
||||
static::$locale = (static::$locale)();
|
||||
}
|
||||
|
||||
if (is_callable(static::$locale) === true) {
|
||||
return static::$locale = (static::$locale)();
|
||||
if (is_string(static::$locale) === true) {
|
||||
return static::$locale;
|
||||
}
|
||||
|
||||
return static::$locale = 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given message
|
||||
* 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 {
|
||||
$locale ??= static::locale();
|
||||
|
||||
if (is_array($key) === true) {
|
||||
// try to use actual locale
|
||||
if ($result = $key[$locale] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
// try to use language code, e.g. `es` when locale is `es_ES`
|
||||
if ($result = $key[Str::before($locale, '_')] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
// use global wildcard as i18n key
|
||||
if (isset($key['*']) === true) {
|
||||
return static::translate($key['*'], $key['*']);
|
||||
}
|
||||
// use fallback
|
||||
if (is_array($fallback) === true) {
|
||||
return
|
||||
$fallback[$locale] ??
|
||||
$fallback['en'] ??
|
||||
reset($fallback);
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
// $key is a string
|
||||
if ($result = static::translation($locale)[$key] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($fallback !== null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
foreach (static::fallbacks() as $fallback) {
|
||||
// skip locales we have already tried
|
||||
if ($locale === $fallback) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($result = static::translation($fallback)[$key] ?? null) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -194,11 +119,122 @@ class I18n
|
||||
}
|
||||
|
||||
$template = static::translate($key, $fallback, $locale);
|
||||
return Str::template($template, $replace, [
|
||||
'fallback' => '-',
|
||||
'start' => '{',
|
||||
'end' => '}'
|
||||
]);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,8 +246,8 @@ class I18n
|
||||
{
|
||||
$locale ??= static::locale();
|
||||
|
||||
if (isset(static::$translations[$locale]) === true) {
|
||||
return static::$translations[$locale];
|
||||
if ($translation = static::$translations[$locale] ?? null) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
if (static::$load instanceof Closure) {
|
||||
@@ -219,9 +255,8 @@ class I18n
|
||||
}
|
||||
|
||||
// try to use language code, e.g. `es` when locale is `es_ES`
|
||||
$lang = Str::before($locale, '_');
|
||||
if (isset(static::$translations[$lang]) === true) {
|
||||
return static::$translations[$lang];
|
||||
if ($translation = static::$translations[Str::before($locale, '_')] ?? null) {
|
||||
return $translation;
|
||||
}
|
||||
|
||||
return static::$translations[$locale] = [];
|
||||
@@ -240,8 +275,8 @@ class I18n
|
||||
*/
|
||||
protected static function decimalNumberFormatter(string $locale): NumberFormatter|null
|
||||
{
|
||||
if (isset(static::$decimalsFormatters[$locale]) === true) {
|
||||
return static::$decimalsFormatters[$locale];
|
||||
if ($formatter = static::$decimalsFormatters[$locale] ?? null) {
|
||||
return $formatter;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -266,8 +301,12 @@ class I18n
|
||||
*
|
||||
* @param bool $formatNumber If set to `false`, the count is not formatted
|
||||
*/
|
||||
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);
|
||||
|
||||
@@ -289,6 +328,6 @@ class I18n
|
||||
$count = static::formatNumber($count, $locale);
|
||||
}
|
||||
|
||||
return str_replace('{{ count }}', $count, $message);
|
||||
return Str::template($message, compact('count'));
|
||||
}
|
||||
}
|
||||
|
@@ -57,7 +57,7 @@ class Iterator implements IteratorAggregate
|
||||
/**
|
||||
* Returns the current element
|
||||
*/
|
||||
public function current()
|
||||
public function current(): mixed
|
||||
{
|
||||
return current($this->data);
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class Iterator implements IteratorAggregate
|
||||
* Moves the cursor to the previous element
|
||||
* and returns it
|
||||
*/
|
||||
public function prev()
|
||||
public function prev(): mixed
|
||||
{
|
||||
return prev($this->data);
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class Iterator implements IteratorAggregate
|
||||
* Moves the cursor to the next element
|
||||
* and returns it
|
||||
*/
|
||||
public function next()
|
||||
public function next(): mixed
|
||||
{
|
||||
return next($this->data);
|
||||
}
|
||||
@@ -110,7 +110,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($needle): int|false
|
||||
public function indexOf(mixed $needle): int|false
|
||||
{
|
||||
return array_search($needle, array_values($this->data));
|
||||
}
|
||||
@@ -121,33 +121,30 @@ class Iterator implements IteratorAggregate
|
||||
* @param mixed $needle the element to search for
|
||||
* @return int|string|false the name of the key or false
|
||||
*/
|
||||
public function keyOf($needle): int|string|false
|
||||
public function keyOf(mixed $needle): int|string|false
|
||||
{
|
||||
return array_search($needle, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks by key if an element is included
|
||||
*
|
||||
* @param mixed $key
|
||||
*/
|
||||
public function has($key): bool
|
||||
public function has(mixed $key): bool
|
||||
{
|
||||
return isset($this->data[$key]) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current key is set
|
||||
*
|
||||
* @param mixed $key the key to check
|
||||
*/
|
||||
public function __isset($key): bool
|
||||
public function __isset(mixed $key): bool
|
||||
{
|
||||
return $this->has($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified var_dump output
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
|
48
kirby/src/Toolkit/LazyValue.php
Normal file
48
kirby/src/Toolkit/LazyValue.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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,8 +21,12 @@ 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'
|
||||
];
|
||||
|
||||
/**
|
||||
|
@@ -34,6 +34,7 @@ class Obj extends stdClass
|
||||
|
||||
/**
|
||||
* Improved `var_dump` output
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
|
@@ -16,10 +16,6 @@ use Kirby\Exception\Exception;
|
||||
*/
|
||||
class Pagination
|
||||
{
|
||||
use Properties {
|
||||
setProperties as protected baseSetProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current page
|
||||
*/
|
||||
@@ -47,7 +43,44 @@ class Pagination
|
||||
*/
|
||||
public function __construct(array $props = [])
|
||||
{
|
||||
$this->setProperties($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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,7 +293,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;
|
||||
|
||||
@@ -294,42 +327,6 @@ class Pagination
|
||||
return array_pop($range);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 setProperties(array $props): static
|
||||
{
|
||||
$this->baseSetProperties($props);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of items per page
|
||||
*
|
||||
|
@@ -7,6 +7,8 @@ use ReflectionMethod;
|
||||
|
||||
/**
|
||||
* Properties
|
||||
* @deprecated 4.0.0 Will be remove in Kirby 5
|
||||
* @codeCoverageIgnore
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
|
@@ -1,251 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Cms\Helpers;
|
||||
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
|
||||
*
|
||||
* @deprecated 3.8.2 Use `Kirby\Query\Query` instead
|
||||
* // TODO: Remove in 3.10.0
|
||||
*/
|
||||
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|null $query = null, $data = [])
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->data = $data;
|
||||
|
||||
Helpers::deprecated('The `Toolkit\Query` class has been deprecated and will be removed in a future version. Use `Query\Query` instead: Kirby\Query\Query::factory($query)->resolve($data).', 'toolkit-query-class');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ($value instanceof Closure) {
|
||||
$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(static::PARAMETERS, $args);
|
||||
$args = array_map([$this, 'parameter'], $args);
|
||||
|
||||
return compact('method', 'args');
|
||||
}
|
||||
|
||||
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([$this, '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,7 +15,7 @@ namespace Kirby\Toolkit;
|
||||
*/
|
||||
class Silo
|
||||
{
|
||||
public static $data = [];
|
||||
public static array $data = [];
|
||||
|
||||
/**
|
||||
* Setter for new data
|
||||
|
@@ -6,6 +6,7 @@ use Closure;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use IntlDateFormatter;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Query\Query;
|
||||
use Throwable;
|
||||
@@ -279,6 +280,17 @@ class Str
|
||||
return lcfirst(static::studly($value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a camel-case string to kebab-case
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param string $value The string to convert
|
||||
*/
|
||||
public static function camelToKebab(string $value = null): string
|
||||
{
|
||||
return static::lower(preg_replace('!([a-z0-9])([A-Z])!', '$1-$2', $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a str contains another string
|
||||
*/
|
||||
@@ -299,12 +311,13 @@ class Str
|
||||
* Convert timestamp to date string
|
||||
* according to locale settings
|
||||
*
|
||||
* @param string $handler date, intl or strftime
|
||||
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
|
||||
* for the globally configured one
|
||||
*/
|
||||
public static function date(
|
||||
int|null $time = null,
|
||||
string|IntlDateFormatter $format = null,
|
||||
string $handler = 'date'
|
||||
string|null $handler = null
|
||||
): string|int|false {
|
||||
if (is_null($format) === true) {
|
||||
return $time;
|
||||
@@ -315,6 +328,13 @@ class Str
|
||||
return $format->format($time ?? time());
|
||||
}
|
||||
|
||||
// automatically determine the handler from global configuration
|
||||
// if an app instance is already running; otherwise fall back to
|
||||
// `date` for backwards-compatibility
|
||||
if ($handler === null) {
|
||||
$handler = App::instance(null, true)?->option('date.handler') ?? 'date';
|
||||
}
|
||||
|
||||
// `intl` handler
|
||||
if ($handler === 'intl') {
|
||||
$datetime = new DateTime();
|
||||
@@ -367,7 +387,8 @@ class Str
|
||||
|
||||
for ($i = 0; $i < static::length($string); $i++) {
|
||||
$char = static::substr($string, $i, 1);
|
||||
list(, $code) = unpack('N', mb_convert_encoding($char, 'UCS-4BE', 'UTF-8'));
|
||||
$char = mb_convert_encoding($char, 'UCS-4BE', 'UTF-8');
|
||||
list(, $code) = unpack('N', $char);
|
||||
$encoded .= rand(1, 2) === 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';';
|
||||
}
|
||||
|
||||
@@ -480,13 +501,21 @@ class Str
|
||||
// make sure $value is not null
|
||||
$value ??= '';
|
||||
|
||||
// turn the value into a string
|
||||
$value = (string)$value;
|
||||
|
||||
// Convert exponential to decimal, 1e-8 as 0.00000001
|
||||
if (strpos(strtolower($value), 'e') !== false) {
|
||||
$value = rtrim(sprintf('%.16f', (float)$value), '0');
|
||||
}
|
||||
|
||||
$value = str_replace(',', '.', $value);
|
||||
$decimal = strlen(substr(strrchr($value, '.'), 1));
|
||||
$decimal = strrchr($value, '.');
|
||||
$decimal = match ($decimal) {
|
||||
false => 0,
|
||||
default => strlen($decimal) - 1
|
||||
};
|
||||
|
||||
return number_format((float)$value, $decimal, '.', '');
|
||||
}
|
||||
|
||||
@@ -538,6 +567,18 @@ class Str
|
||||
return static::snake($value, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a kebab case string to camel case.
|
||||
*/
|
||||
public static function kebabToCamel(string $value = null): string
|
||||
{
|
||||
return ucfirst(preg_replace_callback(
|
||||
'/-(.)/',
|
||||
fn ($matches) => strtoupper($matches[1]),
|
||||
$value ?? ''
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* A UTF-8 safe version of strlen()
|
||||
*/
|
||||
@@ -571,8 +612,12 @@ class Str
|
||||
* @param int $offset Positional offset in the string to start the search
|
||||
* @return array|null The matches or null if no match was found
|
||||
*/
|
||||
public static function match(string $string, string $pattern, int $flags = 0, int $offset = 0): ?array
|
||||
{
|
||||
public static function match(
|
||||
string $string,
|
||||
string $pattern,
|
||||
int $flags = 0,
|
||||
int $offset = 0
|
||||
): array|null {
|
||||
$result = preg_match($pattern, $string, $matches, $flags, $offset);
|
||||
return ($result === 1) ? $matches : null;
|
||||
}
|
||||
@@ -586,8 +631,12 @@ class Str
|
||||
* @param int $offset Positional offset in the string to start the search
|
||||
* @return bool True if the string matches the pattern
|
||||
*/
|
||||
public static function matches(string $string, string $pattern, int $flags = 0, int $offset = 0): bool
|
||||
{
|
||||
public static function matches(
|
||||
string $string,
|
||||
string $pattern,
|
||||
int $flags = 0,
|
||||
int $offset = 0
|
||||
): bool {
|
||||
return static::match($string, $pattern, $flags, $offset) !== null;
|
||||
}
|
||||
|
||||
@@ -600,8 +649,12 @@ class Str
|
||||
* @param int $offset Positional offset in the string to start the search
|
||||
* @return array|null The matches or null if no match was found
|
||||
*/
|
||||
public static function matchAll(string $string, string $pattern, int $flags = 0, int $offset = 0): ?array
|
||||
{
|
||||
public static function matchAll(
|
||||
string $string,
|
||||
string $pattern,
|
||||
int $flags = 0,
|
||||
int $offset = 0
|
||||
): array|null {
|
||||
$result = preg_match_all($pattern, $string, $matches, $flags, $offset);
|
||||
return ($result > 0) ? $matches : null;
|
||||
}
|
||||
@@ -613,9 +666,9 @@ class Str
|
||||
string|array $type,
|
||||
bool $array = true
|
||||
): string|array {
|
||||
$pool = [];
|
||||
|
||||
if (is_array($type) === true) {
|
||||
$pool = [];
|
||||
|
||||
foreach ($type as $t) {
|
||||
$pool = array_merge($pool, static::pool($t));
|
||||
}
|
||||
@@ -628,7 +681,7 @@ class Str
|
||||
'alphanum' => static::pool(['alpha', 'num']),
|
||||
'base32' => array_merge(static::pool('alphaUpper'), range(2, 7)),
|
||||
'base32hex' => array_merge(range(0, 9), range('A', 'V')),
|
||||
default => $pool
|
||||
default => []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -685,7 +738,8 @@ class Str
|
||||
return false;
|
||||
}
|
||||
|
||||
// regex that matches all characters *not* in the pool of allowed characters
|
||||
// regex that matches all characters
|
||||
// *not* in the pool of allowed characters
|
||||
$regex = '/[^' . $pool . ']/';
|
||||
|
||||
// collect characters until we have our required length
|
||||
@@ -694,7 +748,8 @@ class Str
|
||||
while (($currentLength = strlen($result)) < $length) {
|
||||
$missing = $length - $currentLength;
|
||||
$bytes = random_bytes($missing);
|
||||
$result .= substr(preg_replace($regex, '', base64_encode($bytes)), 0, $missing);
|
||||
$allowed = preg_replace($regex, '', base64_encode($bytes));
|
||||
$result .= substr($allowed, 0, $missing);
|
||||
}
|
||||
|
||||
return $result;
|
||||
@@ -704,24 +759,21 @@ class Str
|
||||
* Replaces all or some occurrences of the search string with the replacement string
|
||||
* Extension of the str_replace() function in PHP with an additional $limit parameter
|
||||
*
|
||||
* @param string|array $string String being replaced on (haystack);
|
||||
* can be an array of multiple subject strings
|
||||
* @param string|array $search Value being searched for (needle)
|
||||
* @param string|array $replace Value to replace matches with
|
||||
* @param string|array|Collection $string String being replaced on (haystack); can be an array of multiple subject strings
|
||||
* @param string|array|Collection $search Value being searched for (needle)
|
||||
* @param string|array|Collection $replace Value to replace matches with
|
||||
* @param int|array $limit Maximum possible replacements for each search value;
|
||||
* multiple limits for each search value are supported;
|
||||
* defaults to no limit
|
||||
* @return string|array String with replaced values;
|
||||
* if $string is an array, array of strings
|
||||
* @psalm-return ($string is array ? array : string)
|
||||
*
|
||||
* @todo the types aren't correct, refactor to apply native type hinting
|
||||
*/
|
||||
public static function replace(
|
||||
$string,
|
||||
$search,
|
||||
$replace,
|
||||
$limit = -1
|
||||
string|array|Collection $string,
|
||||
string|array|Collection $search,
|
||||
string|array|Collection $replace,
|
||||
int|array $limit = -1
|
||||
): string|array {
|
||||
// convert Kirby collections to arrays
|
||||
if ($string instanceof Collection) {
|
||||
@@ -749,9 +801,11 @@ class Str
|
||||
// multiple subjects are run separately through this method
|
||||
if (is_array($string) === true) {
|
||||
$result = [];
|
||||
|
||||
foreach ($string as $s) {
|
||||
$result[] = static::replace($s, $search, $replace, $limit);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -775,49 +829,44 @@ class Str
|
||||
* defaults to no limit
|
||||
* @return array List of replacement arrays, each with a
|
||||
* 'search', 'replace' and 'limit' attribute
|
||||
*
|
||||
* @todo the types aren't correct, refactor to apply native type hinting
|
||||
*/
|
||||
public static function replacements(
|
||||
$search,
|
||||
$replace,
|
||||
$limit
|
||||
string|array $search,
|
||||
string|array $replace,
|
||||
int|array $limit
|
||||
): array {
|
||||
$replacements = [];
|
||||
if (is_array($search) === true) {
|
||||
$replacements = [];
|
||||
|
||||
if (is_array($search) === true && is_array($replace) === true) {
|
||||
foreach ($search as $i => $s) {
|
||||
// replace with an empty string if no replacement string was defined for this index;
|
||||
// behavior is identical to the official PHP str_replace()
|
||||
$r = $replace[$i] ?? '';
|
||||
|
||||
if (is_array($limit) === true) {
|
||||
// don't apply a limit if no limit was defined for this index
|
||||
$l = $limit[$i] ?? -1;
|
||||
} else {
|
||||
$l = $limit;
|
||||
if (is_array($replace) === true) {
|
||||
// replace with an empty string if
|
||||
// no replacement string was defined for this index;
|
||||
// behavior is identical to official PHP str_replace()
|
||||
$r = $replace[$i] ?? '';
|
||||
}
|
||||
|
||||
$replacements[] = ['search' => $s, 'replace' => $r, 'limit' => $l];
|
||||
}
|
||||
} elseif (is_array($search) === true && is_string($replace) === true) {
|
||||
foreach ($search as $i => $s) {
|
||||
if (is_array($limit) === true) {
|
||||
// don't apply a limit if no limit was defined for this index
|
||||
// don't apply a limit if no limit
|
||||
// was defined for this index
|
||||
$l = $limit[$i] ?? -1;
|
||||
} else {
|
||||
$l = $limit;
|
||||
}
|
||||
|
||||
$replacements[] = ['search' => $s, 'replace' => $replace, 'limit' => $l];
|
||||
$replacements[] = [
|
||||
'search' => $s,
|
||||
'replace' => $r ?? $replace,
|
||||
'limit' => $l ?? $limit
|
||||
];
|
||||
}
|
||||
} elseif (is_string($search) === true && is_string($replace) === true && is_int($limit) === true) {
|
||||
$replacements[] = compact('search', 'replace', 'limit');
|
||||
} else {
|
||||
throw new Exception('Invalid combination of $search, $replace and $limit params.');
|
||||
|
||||
return $replacements;
|
||||
}
|
||||
|
||||
return $replacements;
|
||||
if (is_string($replace) === true && is_int($limit) === true) {
|
||||
return [compact('search', 'replace', 'limit')];
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Invalid combination of $search, $replace and $limit params.');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -846,15 +895,27 @@ class Str
|
||||
$replacement['replace'],
|
||||
$string
|
||||
);
|
||||
} elseif ($replacement['limit'] > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($replacement['limit'] > 0) {
|
||||
// limit given, only replace for as many times per replacement
|
||||
$position = -1;
|
||||
|
||||
for ($i = 0; $i < $replacement['limit']; $i++) {
|
||||
$position = strpos($string, $replacement['search'], $position + 1);
|
||||
$position = strpos(
|
||||
$string,
|
||||
$replacement['search'],
|
||||
$position + 1
|
||||
);
|
||||
|
||||
if (is_int($position) === true) {
|
||||
$string = substr_replace($string, $replacement['replace'], $position, strlen($replacement['search']));
|
||||
$string = substr_replace(
|
||||
$string,
|
||||
$replacement['replace'],
|
||||
$position,
|
||||
strlen($replacement['search'])
|
||||
);
|
||||
// adapt $pos to the now changed offset
|
||||
$position = $position + strlen($replacement['replace']) - strlen($replacement['search']);
|
||||
} else {
|
||||
@@ -870,10 +931,6 @@ class Str
|
||||
|
||||
/**
|
||||
* Safe rtrim alternative
|
||||
*
|
||||
* @param string $string
|
||||
* @param string $trim
|
||||
* @return string
|
||||
*/
|
||||
public static function rtrim(string $string, string $trim = ' '): string
|
||||
{
|
||||
@@ -1073,11 +1130,19 @@ class Str
|
||||
$string = static::ascii($string);
|
||||
|
||||
// replace spaces with simple dashes
|
||||
$string = preg_replace('![^' . $allowed . ']!i', $separator, $string);
|
||||
$string = preg_replace(
|
||||
'![^' . $allowed . ']!i',
|
||||
$separator,
|
||||
$string
|
||||
);
|
||||
|
||||
if (strlen($separator) > 0) {
|
||||
// remove double separators
|
||||
$string = preg_replace('![' . preg_quote($separator) . ']{2,}!', $separator, $string);
|
||||
$string = preg_replace(
|
||||
'![' . preg_quote($separator) . ']{2,}!',
|
||||
$separator,
|
||||
$string
|
||||
);
|
||||
}
|
||||
|
||||
// replace slashes with dashes
|
||||
@@ -1088,7 +1153,7 @@ class Str
|
||||
$string = preg_replace('![^a-z0-9]+$!', '', $string);
|
||||
|
||||
// cut the string after the given maxlength
|
||||
return static::short($string, $maxlength, false);
|
||||
return static::short($string, $maxlength, '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1134,7 +1199,10 @@ class Str
|
||||
|
||||
foreach ($parts as $p) {
|
||||
$p = trim($p);
|
||||
if (static::length($p) > 0 && static::length($p) >= $length) {
|
||||
if (
|
||||
static::length($p) > 0 &&
|
||||
static::length($p) >= $length
|
||||
) {
|
||||
$out[] = $p;
|
||||
}
|
||||
}
|
||||
@@ -1165,7 +1233,9 @@ class Str
|
||||
*/
|
||||
public static function studly(string $value = null): string
|
||||
{
|
||||
return str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $value)));
|
||||
$value = str_replace(['-', '_'], ' ', $value);
|
||||
$value = ucwords($value);
|
||||
return str_replace(' ', '', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1205,8 +1275,8 @@ class Str
|
||||
array $data = [],
|
||||
array $options = []
|
||||
): string {
|
||||
$start = (string)($options['start'] ?? '{{');
|
||||
$end = (string)($options['end'] ?? '}}');
|
||||
$start = $options['start'] ?? '{{1,2}';
|
||||
$end = $options['end'] ?? '}{1,2}';
|
||||
$fallback = $options['fallback'] ?? null;
|
||||
$callback = $options['callback'] ?? null;
|
||||
|
||||
@@ -1219,7 +1289,7 @@ class Str
|
||||
|
||||
return preg_replace_callback(
|
||||
'!' . $start . '(.*?)' . $end . '!',
|
||||
function ($match) use ($data, $fallback, $callback) {
|
||||
function (array $match) use ($data, $fallback, $callback) {
|
||||
$query = trim($match[1]);
|
||||
|
||||
try {
|
||||
@@ -1233,12 +1303,12 @@ class Str
|
||||
|
||||
// callback on result if given
|
||||
if ($callback !== null) {
|
||||
$callbackResult = $callback((string)$result, $query, $data);
|
||||
$callback = $callback((string)$result, $query, $data);
|
||||
|
||||
if ($result !== null || $callbackResult !== '') {
|
||||
if ($result !== null || $callback !== '') {
|
||||
// the empty string came just from string casting,
|
||||
// keep the null value and ignore the callback result
|
||||
$result = $callbackResult;
|
||||
$result = $callback;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1256,7 +1326,7 @@ class Str
|
||||
public static function toBytes(string $size): int
|
||||
{
|
||||
$size = trim($size);
|
||||
$last = strtolower($size[strlen($size)-1] ?? '');
|
||||
$last = strtolower($size[strlen($size) - 1] ?? '');
|
||||
$size = (int)$size;
|
||||
|
||||
$size *= match ($last) {
|
||||
|
144
kirby/src/Toolkit/Totp.php
Normal file
144
kirby/src/Toolkit/Totp.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ namespace Kirby\Toolkit;
|
||||
|
||||
use Countable;
|
||||
use Exception;
|
||||
use Kirby\Cms\Field;
|
||||
use Kirby\Content\Field;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Http\Idn;
|
||||
use Kirby\Uuid\Uuid;
|
||||
@@ -449,7 +449,7 @@ V::$validators = [
|
||||
* Checks if the value matches the given regular expression
|
||||
*/
|
||||
'match' => function ($value, string $pattern): bool {
|
||||
return preg_match($pattern, $value) !== 0;
|
||||
return preg_match($pattern, (string)$value) === 1;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -596,6 +596,13 @@ 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
|
||||
*/
|
||||
|
@@ -28,8 +28,7 @@ class View
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view's data array
|
||||
* without globals.
|
||||
* Returns the view's data array without globals
|
||||
*/
|
||||
public function data(): array
|
||||
{
|
||||
@@ -71,7 +70,6 @@ class View
|
||||
|
||||
ob_start();
|
||||
|
||||
$exception = null;
|
||||
try {
|
||||
F::load($this->file(), null, $this->data());
|
||||
} catch (Throwable $e) {
|
||||
@@ -81,11 +79,11 @@ class View
|
||||
$content = ob_get_contents();
|
||||
ob_end_clean();
|
||||
|
||||
if ($exception === null) {
|
||||
return $content;
|
||||
if (($exception ?? null) !== null) {
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,6 +97,8 @@ class View
|
||||
/**
|
||||
* Magic string converter to enable
|
||||
* converting view objects to string
|
||||
*
|
||||
* @see ::render()
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
|
@@ -94,32 +94,23 @@ class Xml
|
||||
return implode(' ', $attributes);
|
||||
}
|
||||
|
||||
// TODO: In 3.10, treat $value === '' to render as name=""
|
||||
if ($value === null || $value === '' || $value === []) {
|
||||
// TODO: Remove in 3.10
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($value === '') {
|
||||
Helpers::deprecated('Passing an empty string as value to `Xml::attr()` has been deprecated. In a future version, passing an empty string won\'t omit the attribute anymore but render it with an empty value. To omit the attribute, please pass `null`.', 'xml-attr-empty-string');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ($value === null || $value === false || $value === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: In 3.10, add deprecation message for space = empty attribute
|
||||
// TODO: In 3.11, render space as space
|
||||
// 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 . '=""';
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
if ($value === true) {
|
||||
return $name . '="' . $name . '"';
|
||||
}
|
||||
|
||||
if ($value === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value) === true) {
|
||||
if (isset($value['value'], $value['escape'])) {
|
||||
$value = $value['escape'] === true ? static::encode($value['value']) : $value['value'];
|
||||
|
Reference in New Issue
Block a user