Upgrade to rc5
This commit is contained in:
@@ -52,7 +52,7 @@ class Collection
|
||||
* @param array $schema
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(Api $api, $data = null, array $schema)
|
||||
public function __construct(Api $api, $data, array $schema)
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->data = $data;
|
||||
|
@@ -50,11 +50,11 @@ class Model
|
||||
* Model constructor
|
||||
*
|
||||
* @param \Kirby\Api\Api $api
|
||||
* @param null $data
|
||||
* @param mixed $data
|
||||
* @param array $schema
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(Api $api, $data = null, array $schema)
|
||||
public function __construct(Api $api, $data, array $schema)
|
||||
{
|
||||
$this->api = $api;
|
||||
$this->data = $data;
|
||||
|
@@ -56,28 +56,7 @@ class Api extends BaseApi
|
||||
*/
|
||||
public function fieldApi($model, string $name, string $path = null)
|
||||
{
|
||||
$form = Form::for($model);
|
||||
$fieldNames = Str::split($name, '+');
|
||||
$index = 0;
|
||||
$count = count($fieldNames);
|
||||
$field = null;
|
||||
|
||||
foreach ($fieldNames as $fieldName) {
|
||||
$index++;
|
||||
|
||||
if ($field = $form->fields()->get($fieldName)) {
|
||||
if ($count !== $index) {
|
||||
$form = $field->form();
|
||||
}
|
||||
} else {
|
||||
throw new NotFoundException('The field "' . $fieldName . '" could not be found');
|
||||
}
|
||||
}
|
||||
|
||||
// it can get this error only if $name is an empty string as $name = ''
|
||||
if ($field === null) {
|
||||
throw new NotFoundException('No field could be loaded');
|
||||
}
|
||||
$field = Form::for($model)->field($name);
|
||||
|
||||
$fieldApi = $this->clone([
|
||||
'routes' => $field->api(),
|
||||
|
@@ -79,8 +79,9 @@ class App
|
||||
* Creates a new App instance
|
||||
*
|
||||
* @param array $props
|
||||
* @param bool $setInstance If false, the instance won't be set globally
|
||||
*/
|
||||
public function __construct(array $props = [])
|
||||
public function __construct(array $props = [], bool $setInstance = true)
|
||||
{
|
||||
// register all roots to be able to load stuff afterwards
|
||||
$this->bakeRoots($props['roots'] ?? []);
|
||||
@@ -110,7 +111,9 @@ class App
|
||||
]);
|
||||
|
||||
// set the singleton
|
||||
Model::$kirby = static::$instance = $this;
|
||||
if (static::$instance === null || $setInstance === true) {
|
||||
Model::$kirby = static::$instance = $this;
|
||||
}
|
||||
|
||||
// setup the I18n class with the translation loader
|
||||
$this->i18n();
|
||||
@@ -333,6 +336,24 @@ class App
|
||||
return $router->call($path ?? $this->path(), $method ?? $this->request()->method());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance with the same
|
||||
* initial properties
|
||||
*
|
||||
* @param array $props
|
||||
* @param bool $setInstance If false, the instance won't be set globally
|
||||
* @return self
|
||||
*/
|
||||
public function clone(array $props = [], bool $setInstance = true)
|
||||
{
|
||||
$props = array_replace_recursive($this->propertyData, $props);
|
||||
|
||||
$clone = new static($props, $setInstance);
|
||||
$clone->data = $this->data;
|
||||
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific user-defined collection
|
||||
* by name. All relevant dependencies are
|
||||
|
@@ -48,6 +48,7 @@ trait AppPlugins
|
||||
|
||||
// other plugin types
|
||||
'api' => [],
|
||||
'authChallenges' => [],
|
||||
'blueprints' => [],
|
||||
'cacheTypes' => [],
|
||||
'collections' => [],
|
||||
@@ -125,6 +126,17 @@ trait AppPlugins
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional authentication challenges
|
||||
*
|
||||
* @param array $challenges
|
||||
* @return array
|
||||
*/
|
||||
protected function extendAuthChallenges(array $challenges): array
|
||||
{
|
||||
return $this->extensions['authChallenges'] = Auth::$challenges = array_merge(Auth::$challenges, $challenges);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers additional blueprints
|
||||
*
|
||||
@@ -618,6 +630,7 @@ trait AppPlugins
|
||||
// load static extensions only once
|
||||
if (static::$systemExtensions === null) {
|
||||
// Form Field Mixins
|
||||
FormField::$mixins['datetime'] = include $root . '/config/fields/mixins/datetime.php';
|
||||
FormField::$mixins['filepicker'] = include $root . '/config/fields/mixins/filepicker.php';
|
||||
FormField::$mixins['min'] = include $root . '/config/fields/mixins/min.php';
|
||||
FormField::$mixins['options'] = include $root . '/config/fields/mixins/options.php';
|
||||
@@ -675,10 +688,17 @@ trait AppPlugins
|
||||
'blueprints' => include $root . '/config/blueprints.php',
|
||||
'fields' => include $root . '/config/fields.php',
|
||||
'fieldMethods' => include $root . '/config/methods.php',
|
||||
'tags' => include $root . '/config/tags.php'
|
||||
'snippets' => include $root . '/config/snippets.php',
|
||||
'tags' => include $root . '/config/tags.php',
|
||||
'templates' => include $root . '/config/templates.php'
|
||||
];
|
||||
}
|
||||
|
||||
// default auth challenges
|
||||
$this->extendAuthChallenges([
|
||||
'email' => 'Kirby\Cms\Auth\EmailChallenge'
|
||||
]);
|
||||
|
||||
// default cache types
|
||||
$this->extendCacheTypes([
|
||||
'apcu' => 'Kirby\Cache\ApcuCache',
|
||||
@@ -691,7 +711,9 @@ trait AppPlugins
|
||||
$this->extendBlueprints(static::$systemExtensions['blueprints']);
|
||||
$this->extendFields(static::$systemExtensions['fields']);
|
||||
$this->extendFieldMethods((static::$systemExtensions['fieldMethods'])($this));
|
||||
$this->extendSnippets(static::$systemExtensions['snippets']);
|
||||
$this->extendTags(static::$systemExtensions['tags']);
|
||||
$this->extendTemplates(static::$systemExtensions['templates']);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,10 +2,9 @@
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Locale;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
@@ -36,11 +35,11 @@ trait AppTranslations
|
||||
}
|
||||
|
||||
// inject translations from the current language
|
||||
if ($this->multilang() === true && $language = $this->languages()->find($locale)) {
|
||||
if (
|
||||
$this->multilang() === true &&
|
||||
$language = $this->languages()->find($locale)
|
||||
) {
|
||||
$data = array_merge($data, $language->translations());
|
||||
|
||||
// Add language slug rules to Str class
|
||||
Str::$language = $language->rules();
|
||||
}
|
||||
|
||||
|
||||
@@ -65,25 +64,12 @@ trait AppTranslations
|
||||
|
||||
I18n::$translations = [];
|
||||
|
||||
// checks custom language definition for slugs
|
||||
if ($slugsOption = $this->option('slugs')) {
|
||||
// checks setting in two different ways
|
||||
// add slug rules based on config option
|
||||
if ($slugs = $this->option('slugs')) {
|
||||
// two ways that the option can be defined:
|
||||
// "slugs" => "de" or "slugs" => ["language" => "de"]
|
||||
$slugsLanguage = is_string($slugsOption) === true ? $slugsOption : ($slugsOption['language'] ?? null);
|
||||
|
||||
// load custom slugs language if it's defined
|
||||
if ($slugsLanguage !== null) {
|
||||
$file = $this->root('i18n:rules') . '/' . $slugsLanguage . '.json';
|
||||
|
||||
if (F::exists($file) === true) {
|
||||
try {
|
||||
$data = Data::read($file);
|
||||
} catch (Exception $e) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
Str::$language = $data;
|
||||
}
|
||||
if ($slugs = $slugs['language'] ?? $slugs ?? null) {
|
||||
Str::$language = Language::loadRules($slugs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +85,7 @@ trait AppTranslations
|
||||
public function setCurrentLanguage(string $languageCode = null)
|
||||
{
|
||||
if ($this->multilang() === false) {
|
||||
$this->setLocale($this->option('locale', 'en_US.utf-8'));
|
||||
Locale::set($this->option('locale', 'en_US.utf-8'));
|
||||
return $this->language = null;
|
||||
}
|
||||
|
||||
@@ -110,9 +96,12 @@ trait AppTranslations
|
||||
}
|
||||
|
||||
if ($this->language) {
|
||||
$this->setLocale($this->language->locale());
|
||||
Locale::set($this->language->locale());
|
||||
}
|
||||
|
||||
// add language slug rules to Str class
|
||||
Str::$language = $this->language->rules();
|
||||
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
@@ -131,18 +120,13 @@ trait AppTranslations
|
||||
/**
|
||||
* Set locale settings
|
||||
*
|
||||
* @internal
|
||||
* @deprecated 3.5.0 Use \Kirby\Toolkit\Locale::set() instead
|
||||
*
|
||||
* @param string|array $locale
|
||||
*/
|
||||
public function setLocale($locale): void
|
||||
{
|
||||
if (is_array($locale) === true) {
|
||||
foreach ($locale as $key => $value) {
|
||||
setlocale($key, $value);
|
||||
}
|
||||
} else {
|
||||
setlocale(LC_ALL, $locale);
|
||||
}
|
||||
Locale::set($locale);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -35,9 +35,12 @@ trait AppUsers
|
||||
}
|
||||
|
||||
/**
|
||||
* Become any existing user
|
||||
* Become any existing user or disable the current user
|
||||
*
|
||||
* @param string|null $who User ID or email address
|
||||
* @param string|null $who User ID or email address,
|
||||
* `null` to use the actual user again,
|
||||
* `'kirby'` for a virtual admin user or
|
||||
* `'nobody'` to disable the actual user
|
||||
* @param Closure|null $callback Optional action function that will be run with
|
||||
* the permissions of the impersonated user; the
|
||||
* impersonation will be reset afterwards
|
||||
|
@@ -4,10 +4,12 @@ namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Http\Idn;
|
||||
use Kirby\Http\Request\Auth\BasicAuth;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\F;
|
||||
use Throwable;
|
||||
|
||||
@@ -22,6 +24,14 @@ use Throwable;
|
||||
*/
|
||||
class Auth
|
||||
{
|
||||
/**
|
||||
* Available auth challenge classes
|
||||
* from the core and plugins
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $challenges = [];
|
||||
|
||||
protected $impersonate;
|
||||
protected $kirby;
|
||||
protected $user = false;
|
||||
@@ -36,6 +46,105 @@ class Auth
|
||||
$this->kirby = $kirby;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an authentication challenge
|
||||
* (one-time auth code)
|
||||
*
|
||||
* @param string $email
|
||||
* @param bool $long If `true`, a long session will be created
|
||||
* @param string $mode Either 'login' or 'password-reset'
|
||||
* @return string|null Name of the challenge that was created;
|
||||
* `null` if the user does not exist or no
|
||||
* challenge was available for the user
|
||||
*
|
||||
* @throws \Kirby\Exception\LogicException If there is no suitable authentication challenge (only in debug mode)
|
||||
* @throws \Kirby\Exception\NotFoundException If the user does not exist (only in debug mode)
|
||||
* @throws \Kirby\Exception\PermissionException If the rate limit is exceeded
|
||||
*/
|
||||
public function createChallenge(string $email, bool $long = false, string $mode = 'login'): ?string
|
||||
{
|
||||
// ensure that email addresses with IDN domains are in Unicode format
|
||||
$email = Idn::decodeEmail($email);
|
||||
|
||||
if ($this->isBlocked($email) === true) {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
$message = 'Rate limit exceeded';
|
||||
} else {
|
||||
// avoid leaking security-relevant information
|
||||
$message = ['key' => 'access.login'];
|
||||
}
|
||||
|
||||
throw new PermissionException($message);
|
||||
}
|
||||
|
||||
// rate-limit the number of challenges for DoS/DDoS protection
|
||||
$this->track($email, false);
|
||||
|
||||
$session = $this->kirby->session([
|
||||
'createMode' => 'cookie',
|
||||
'long' => $long === true
|
||||
]);
|
||||
|
||||
$challenge = null;
|
||||
if ($user = $this->kirby->users()->find($email)) {
|
||||
$timeout = $this->kirby->option('auth.challenge.timeout', 10 * 60);
|
||||
|
||||
$challenges = $this->kirby->option('auth.challenges', ['email']);
|
||||
foreach (A::wrap($challenges) as $name) {
|
||||
$class = static::$challenges[$name] ?? null;
|
||||
if (
|
||||
$class &&
|
||||
class_exists($class) === true &&
|
||||
is_subclass_of($class, 'Kirby\Cms\Auth\Challenge') === true &&
|
||||
$class::isAvailable($user, $mode) === true
|
||||
) {
|
||||
$challenge = $name;
|
||||
$code = $class::create($user, compact('mode', 'timeout'));
|
||||
|
||||
$session->set('kirby.challenge.type', $challenge);
|
||||
|
||||
if ($code !== null) {
|
||||
$session->set('kirby.challenge.code', password_hash($code, PASSWORD_DEFAULT));
|
||||
$session->set('kirby.challenge.timeout', time() + $timeout);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if no suitable challenge was found, `$challenge === null` at this point;
|
||||
// only leak this in debug mode, otherwise `null` is returned below
|
||||
if ($challenge === null && $this->kirby->option('debug') === true) {
|
||||
throw new LogicException('Could not find a suitable authentication challenge');
|
||||
}
|
||||
} else {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
|
||||
// only leak the non-existing user in debug mode
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $email
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// always set the email, even if the challenge won't be
|
||||
// created to avoid leaking whether the user exists
|
||||
$session->set('kirby.challenge.email', $email);
|
||||
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder and to
|
||||
// avoid leaking whether the user exists
|
||||
usleep(random_int(1000, 300000));
|
||||
|
||||
return $challenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the csrf token if it exists and if it is valid
|
||||
*
|
||||
@@ -73,6 +182,19 @@ class Auth
|
||||
throw new PermissionException('Basic authentication is not activated');
|
||||
}
|
||||
|
||||
// if logging in with password is disabled, basic auth cannot be possible either
|
||||
$loginMethods = $this->kirby->system()->loginMethods();
|
||||
if (isset($loginMethods['password']) !== true) {
|
||||
throw new PermissionException('Login with password is not enabled');
|
||||
}
|
||||
|
||||
// if any login method requires 2FA, basic auth without 2FA would be a weakness
|
||||
foreach ($loginMethods as $method) {
|
||||
if (isset($method['2fa']) === true && $method['2fa'] === true) {
|
||||
throw new PermissionException('Basic authentication cannot be used with 2FA');
|
||||
}
|
||||
}
|
||||
|
||||
$request = $this->kirby->request();
|
||||
$auth = $auth ?? $request->auth();
|
||||
|
||||
@@ -118,7 +240,7 @@ class Auth
|
||||
$session = $this->kirby->session(['detect' => true]);
|
||||
}
|
||||
|
||||
$id = $session->data()->get('user.id');
|
||||
$id = $session->data()->get('kirby.userId');
|
||||
|
||||
if (is_string($id) !== true) {
|
||||
return null;
|
||||
@@ -135,9 +257,12 @@ class Auth
|
||||
}
|
||||
|
||||
/**
|
||||
* Become any existing user
|
||||
* Become any existing user or disable the current user
|
||||
*
|
||||
* @param string|null $who User ID or email address
|
||||
* @param string|null $who User ID or email address,
|
||||
* `null` to use the actual user again,
|
||||
* `'kirby'` for a virtual admin user or
|
||||
* `'nobody'` to disable the actual user
|
||||
* @return \Kirby\Cms\User|null
|
||||
* @throws \Kirby\Exception\NotFoundException if the given user cannot be found
|
||||
*/
|
||||
@@ -152,6 +277,12 @@ class Auth
|
||||
'id' => 'kirby',
|
||||
'role' => 'admin',
|
||||
]);
|
||||
case 'nobody':
|
||||
return $this->impersonate = new User([
|
||||
'email' => 'nobody@getkirby.com',
|
||||
'id' => 'nobody',
|
||||
'role' => 'nobody',
|
||||
]);
|
||||
default:
|
||||
if ($user = $this->kirby->users()->find($who)) {
|
||||
return $this->impersonate = $user;
|
||||
@@ -231,6 +362,25 @@ class Auth
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login a user by email, password and auth challenge
|
||||
*
|
||||
* @param string $email
|
||||
* @param string $password
|
||||
* @param bool $long
|
||||
* @return string|null Name of the challenge that was created;
|
||||
* `null` if no challenge was available for the user
|
||||
*
|
||||
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occured with debug mode off
|
||||
* @throws \Kirby\Exception\NotFoundException If the email was invalid
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
|
||||
*/
|
||||
public function login2fa(string $email, string $password, bool $long = false)
|
||||
{
|
||||
$this->validatePassword($email, $password);
|
||||
return $this->createChallenge($email, $long, '2fa');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a user object as the current user in the cache
|
||||
* @internal
|
||||
@@ -271,7 +421,7 @@ class Auth
|
||||
$message = 'Rate limit exceeded';
|
||||
} else {
|
||||
// avoid leaking security-relevant information
|
||||
$message = 'Invalid email or password';
|
||||
$message = ['key' => 'access.login'];
|
||||
}
|
||||
|
||||
throw new PermissionException($message);
|
||||
@@ -304,7 +454,7 @@ class Auth
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
throw $e;
|
||||
} else {
|
||||
throw new PermissionException('Invalid email or password');
|
||||
throw new PermissionException(['key' => 'access.login']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -377,6 +527,13 @@ class Auth
|
||||
if ($user = $this->user()) {
|
||||
$user->logout();
|
||||
}
|
||||
|
||||
// clear the pending challenge
|
||||
$session = $this->kirby->session();
|
||||
$session->remove('kirby.challenge.code');
|
||||
$session->remove('kirby.challenge.email');
|
||||
$session->remove('kirby.challenge.timeout');
|
||||
$session->remove('kirby.challenge.type');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -394,12 +551,15 @@ class Auth
|
||||
/**
|
||||
* Tracks a login
|
||||
*
|
||||
* @param string $email
|
||||
* @param string|null $email
|
||||
* @param bool $triggerHook If `false`, no user.login:failed hook is triggered
|
||||
* @return bool
|
||||
*/
|
||||
public function track(string $email): bool
|
||||
public function track(?string $email, bool $triggerHook = true): bool
|
||||
{
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
if ($triggerHook === true) {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
}
|
||||
|
||||
$ip = $this->ipHash();
|
||||
$log = $this->log();
|
||||
@@ -417,7 +577,7 @@ class Auth
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->kirby->users()->find($email)) {
|
||||
if ($email !== null && $this->kirby->users()->find($email)) {
|
||||
if (isset($log['by-email'][$email]) === true) {
|
||||
$log['by-email'][$email] = [
|
||||
'time' => $time,
|
||||
@@ -462,7 +622,7 @@ class Auth
|
||||
* @param \Kirby\Session\Session|array|null $session
|
||||
* @param bool $allowImpersonation If set to false, only the actually
|
||||
* logged in user will be returned
|
||||
* @return \Kirby\Cms\User
|
||||
* @return \Kirby\Cms\User|null
|
||||
*
|
||||
* @throws \Throwable If an authentication error occured
|
||||
*/
|
||||
@@ -499,4 +659,89 @@ class Auth
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an authentication code that was
|
||||
* requested with the `createChallenge()` method;
|
||||
* if successful, the user is automatically logged in
|
||||
*
|
||||
* @param string $code User-provided auth code to verify
|
||||
* @return \Kirby\Cms\User User object of the logged-in user
|
||||
*
|
||||
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded, the challenge timed out, the code
|
||||
* is incorrect or if any other error occured with debug mode off
|
||||
* @throws \Kirby\Exception\NotFoundException If the user from the challenge doesn't exist
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If no authentication challenge is active
|
||||
* @throws \Kirby\Exception\LogicException If the authentication challenge is invalid
|
||||
*/
|
||||
public function verifyChallenge(string $code)
|
||||
{
|
||||
try {
|
||||
$session = $this->kirby->session();
|
||||
|
||||
// first check if we have an active challenge at all
|
||||
$email = $session->get('kirby.challenge.email');
|
||||
$challenge = $session->get('kirby.challenge.type');
|
||||
if (is_string($email) !== true || is_string($challenge) !== true) {
|
||||
throw new InvalidArgumentException('No authentication challenge is active');
|
||||
}
|
||||
|
||||
$user = $this->kirby->users()->find($email);
|
||||
if ($user === null) {
|
||||
throw new NotFoundException([
|
||||
'key' => 'user.notFound',
|
||||
'data' => [
|
||||
'name' => $email
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
// rate-limiting
|
||||
if ($this->isBlocked($email) === true) {
|
||||
$this->kirby->trigger('user.login:failed', compact('email'));
|
||||
throw new PermissionException('Rate limit exceeded');
|
||||
}
|
||||
|
||||
// time-limiting
|
||||
$timeout = $session->get('kirby.challenge.timeout');
|
||||
if ($timeout !== null && time() > $timeout) {
|
||||
throw new PermissionException('Authentication challenge timeout');
|
||||
}
|
||||
|
||||
if (
|
||||
isset(static::$challenges[$challenge]) === true &&
|
||||
class_exists(static::$challenges[$challenge]) === true &&
|
||||
is_subclass_of(static::$challenges[$challenge], 'Kirby\Cms\Auth\Challenge') === true
|
||||
) {
|
||||
$class = static::$challenges[$challenge];
|
||||
if ($class::verify($user, $code) === true) {
|
||||
$this->logout();
|
||||
$user->loginPasswordless();
|
||||
|
||||
return $user;
|
||||
} else {
|
||||
throw new PermissionException(['key' => 'access.code']);
|
||||
}
|
||||
}
|
||||
|
||||
throw new LogicException('Invalid authentication challenge: ' . $challenge);
|
||||
} catch (Throwable $e) {
|
||||
if ($e->getMessage() !== 'Rate limit exceeded') {
|
||||
$this->track($email);
|
||||
}
|
||||
|
||||
// sleep for a random amount of milliseconds
|
||||
// to make automated attacks harder and to
|
||||
// avoid leaking whether the user exists
|
||||
usleep(random_int(1000, 2000000));
|
||||
|
||||
// keep throwing the original error in debug mode,
|
||||
// otherwise hide it to avoid leaking security-relevant information
|
||||
if ($this->kirby->option('debug') === true) {
|
||||
throw $e;
|
||||
} else {
|
||||
throw new PermissionException(['key' => 'access.code']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
63
kirby/src/Cms/Auth/Challenge.php
Executable file
63
kirby/src/Cms/Auth/Challenge.php
Executable file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms\Auth;
|
||||
|
||||
use Kirby\Cms\User;
|
||||
|
||||
/**
|
||||
* Template class for authentication challenges
|
||||
* that create and verify one-time auth codes
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
abstract class Challenge
|
||||
{
|
||||
/**
|
||||
* Checks whether the challenge is available
|
||||
* for the passed user and purpose
|
||||
*
|
||||
* @param \Kirby\Cms\User $user User the code will be generated for
|
||||
* @param string $mode Purpose of the code ('login', 'reset' or '2fa')
|
||||
* @return bool
|
||||
*/
|
||||
abstract public static function isAvailable(User $user, string $mode): bool;
|
||||
|
||||
/**
|
||||
* Generates a random one-time auth code and returns that code
|
||||
* for later verification
|
||||
*
|
||||
* @param \Kirby\Cms\User $user User to generate the code for
|
||||
* @param array $options Details of the challenge request:
|
||||
* - 'mode': Purpose of the code ('login', 'reset' or '2fa')
|
||||
* - 'timeout': Number of seconds the code will be valid for
|
||||
* @return string|null The generated and sent code or `null` in case
|
||||
* there was no code to generate by this algorithm
|
||||
*/
|
||||
abstract public static function create(User $user, array $options): ?string;
|
||||
|
||||
/**
|
||||
* Verifies the provided code against the created one;
|
||||
* default implementation that checks the code that was
|
||||
* returned from the `create()` method
|
||||
*
|
||||
* @param \Kirby\Cms\User $user User to check the code for
|
||||
* @param string $code Code to verify
|
||||
* @return bool
|
||||
*/
|
||||
public static function verify(User $user, string $code): bool
|
||||
{
|
||||
$hash = $user->kirby()->session()->get('kirby.challenge.code');
|
||||
if (is_string($hash) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// normalize the formatting in the user-provided code
|
||||
$code = str_replace(' ', '', $code);
|
||||
|
||||
return password_verify($code, $hash);
|
||||
}
|
||||
}
|
76
kirby/src/Cms/Auth/EmailChallenge.php
Executable file
76
kirby/src/Cms/Auth/EmailChallenge.php
Executable file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms\Auth;
|
||||
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Creates and verifies one-time auth codes
|
||||
* that are sent via email
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class EmailChallenge extends Challenge
|
||||
{
|
||||
/**
|
||||
* Checks whether the challenge is available
|
||||
* for the passed user and purpose
|
||||
*
|
||||
* @param \Kirby\Cms\User $user User the code will be generated for
|
||||
* @param string $mode Purpose of the code ('login', 'reset' or '2fa')
|
||||
* @return bool
|
||||
*/
|
||||
public static function isAvailable(User $user, string $mode): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random one-time auth code and returns that code
|
||||
* for later verification
|
||||
*
|
||||
* @param \Kirby\Cms\User $user User to generate the code for
|
||||
* @param array $options Details of the challenge request:
|
||||
* - 'mode': Purpose of the code ('login', 'reset' or '2fa')
|
||||
* - 'timeout': Number of seconds the code will be valid for
|
||||
* @return string The generated and sent code
|
||||
*/
|
||||
public static function create(User $user, array $options): string
|
||||
{
|
||||
$code = Str::random(6, 'num');
|
||||
|
||||
// insert a space in the middle for easier readability
|
||||
$formatted = substr($code, 0, 3) . ' ' . substr($code, 3, 3);
|
||||
|
||||
// use the login templates for 2FA
|
||||
$mode = $options['mode'];
|
||||
if ($mode === '2fa') {
|
||||
$mode = 'login';
|
||||
}
|
||||
|
||||
$kirby = $user->kirby();
|
||||
$kirby->email([
|
||||
'from' => $kirby->option('auth.challenge.email.from', 'noreply@' . $kirby->system()->indexUrl()),
|
||||
'fromName' => $kirby->option('auth.challenge.email.fromName', $kirby->site()->title()),
|
||||
'to' => $user,
|
||||
'subject' => $kirby->option(
|
||||
'auth.challenge.email.subject',
|
||||
I18n::translate('login.email.' . $mode . '.subject')
|
||||
),
|
||||
'template' => 'auth/' . $mode,
|
||||
'data' => [
|
||||
'user' => $user,
|
||||
'code' => $formatted,
|
||||
'timeout' => round($options['timeout'] / 60)
|
||||
]
|
||||
]);
|
||||
|
||||
return $code;
|
||||
}
|
||||
}
|
217
kirby/src/Cms/Block.php
Executable file
217
kirby/src/Cms/Block.php
Executable file
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Represents a single block
|
||||
* which can be inspected further or
|
||||
* converted to HTML
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Block extends Item
|
||||
{
|
||||
const ITEMS_CLASS = '\Kirby\Cms\Blocks';
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Content
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $isHidden;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* Proxy for content fields
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return \Kirby\Cms\Field
|
||||
*/
|
||||
public function __call(string $method, array $args = [])
|
||||
{
|
||||
return $this->content()->get($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new block object
|
||||
*
|
||||
* @param array $params
|
||||
* @param \Kirby\Cms\Blocks $siblings
|
||||
*/
|
||||
public function __construct(array $params)
|
||||
{
|
||||
parent::__construct($params);
|
||||
|
||||
// import old builder format
|
||||
$params = BlockConverter::builderBlock($params);
|
||||
$params = BlockConverter::editorBlock($params);
|
||||
|
||||
if (isset($params['type']) === false) {
|
||||
throw new InvalidArgumentException('The block type is missing');
|
||||
}
|
||||
|
||||
$this->content = $params['content'] ?? [];
|
||||
$this->isHidden = $params['isHidden'] ?? false;
|
||||
$this->type = $params['type'] ?? null;
|
||||
|
||||
// create the content object
|
||||
$this->content = new Content($this->content, $this->parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object to a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated method to return the block type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function _key(): string
|
||||
{
|
||||
return $this->type();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated method to return the block id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function _uid(): string
|
||||
{
|
||||
return $this->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content object
|
||||
*
|
||||
* @return \Kirby\Cms\Content
|
||||
*/
|
||||
public function content()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller for the block snippet
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function controller(): array
|
||||
{
|
||||
return [
|
||||
'block' => $this,
|
||||
'content' => $this->content(),
|
||||
// deprecated block data
|
||||
'data' => $this,
|
||||
'id' => $this->id(),
|
||||
'prev' => $this->prev(),
|
||||
'next' => $this->next()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the block is empty
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return empty($this->content()->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the block is hidden
|
||||
* from being rendered in the frontend
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isHidden(): bool
|
||||
{
|
||||
return $this->isHidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the block is not empty
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNotEmpty(): bool
|
||||
{
|
||||
return $this->isEmpty() === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the block type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result is being sent to the editor
|
||||
* via the API in the panel
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'content' => $this->content()->toArray(),
|
||||
'id' => $this->id(),
|
||||
'isHidden' => $this->isHidden(),
|
||||
'type' => $this->type(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the block to html first
|
||||
* and then places that inside a field
|
||||
* object. This can be used further
|
||||
* with all available field methods
|
||||
*
|
||||
* @return \Kirby\Cms\Field;
|
||||
*/
|
||||
public function toField()
|
||||
{
|
||||
return new Field($this->parent(), $this->id(), $this->toHtml());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the block to HTML
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toHtml(): string
|
||||
{
|
||||
try {
|
||||
return (string)snippet('blocks/' . $this->type(), $this->controller(), true);
|
||||
} catch (Throwable $e) {
|
||||
return '<p>Block error: "' . $e->getMessage() . '" in block type: "' . $this->type() . '"</p>';
|
||||
}
|
||||
}
|
||||
}
|
258
kirby/src/Cms/BlockConverter.php
Executable file
258
kirby/src/Cms/BlockConverter.php
Executable file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
class BlockConverter
|
||||
{
|
||||
public static function builderBlock(array $params): array
|
||||
{
|
||||
if (isset($params['_key']) === false) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$params['type'] = $params['_key'];
|
||||
$params['content'] = $params;
|
||||
unset($params['_uid']);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public static function editorBlock(array $params): array
|
||||
{
|
||||
if (static::isEditorBlock($params) === false) {
|
||||
return $params;
|
||||
}
|
||||
|
||||
$method = 'editor' . $params['type'];
|
||||
|
||||
if (method_exists(static::class, $method) === true) {
|
||||
$params = static::$method($params);
|
||||
} else {
|
||||
$params = static::editorCustom($params);
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
public static function editorBlocks(array $blocks = []): array
|
||||
{
|
||||
if (empty($blocks) === true) {
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
if (static::isEditorBlock($blocks[0]) === false) {
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
$list = [];
|
||||
$listStart = null;
|
||||
|
||||
foreach ($blocks as $index => $block) {
|
||||
if (in_array($block['type'], ['ul', 'ol']) === true) {
|
||||
$prev = $blocks[$index-1] ?? null;
|
||||
$next = $blocks[$index+1] ?? null;
|
||||
|
||||
// new list starts here
|
||||
if (!$prev || $prev['type'] !== $block['type']) {
|
||||
$listStart = $index;
|
||||
}
|
||||
|
||||
// add the block to the list
|
||||
$list[] = $block;
|
||||
|
||||
// list ends here
|
||||
if (!$next || $next['type'] !== $block['type']) {
|
||||
$blocks[$listStart] = [
|
||||
'content' => [
|
||||
'text' =>
|
||||
'<' . $block['type'] . '>' .
|
||||
implode(array_map(function ($item) {
|
||||
return '<li>' . $item['content'] . '</li>';
|
||||
}, $list)) .
|
||||
'</' . $block['type'] . '>',
|
||||
],
|
||||
'type' => 'list'
|
||||
];
|
||||
|
||||
for ($x = $listStart+1; $x <= $listStart + count($list); $x++) {
|
||||
$blocks[$x] = false;
|
||||
}
|
||||
|
||||
$listStart = null;
|
||||
$list = [];
|
||||
}
|
||||
} else {
|
||||
$blocks[$index] = static::editorBlock($block);
|
||||
}
|
||||
}
|
||||
|
||||
return array_filter($blocks);
|
||||
}
|
||||
|
||||
public static function editorBlockquote(array $params): array
|
||||
{
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $params['content']
|
||||
],
|
||||
'type' => 'quote'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorCode(array $params): array
|
||||
{
|
||||
return [
|
||||
'content' => [
|
||||
'language' => $params['attrs']['language'] ?? null,
|
||||
'code' => $params['content']
|
||||
],
|
||||
'type' => 'code'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorCustom(array $params): array
|
||||
{
|
||||
return [
|
||||
'content' => array_merge(
|
||||
$params['attrs'] ?? [],
|
||||
[
|
||||
'body' => $params['content'] ?? null
|
||||
]
|
||||
),
|
||||
'type' => $params['type'] ?? 'unknown'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorH1(array $params): array
|
||||
{
|
||||
return static::editorHeading($params, 'h1');
|
||||
}
|
||||
|
||||
public static function editorH2(array $params): array
|
||||
{
|
||||
return static::editorHeading($params, 'h2');
|
||||
}
|
||||
|
||||
public static function editorH3(array $params): array
|
||||
{
|
||||
return static::editorHeading($params, 'h3');
|
||||
}
|
||||
|
||||
public static function editorH4(array $params): array
|
||||
{
|
||||
return static::editorHeading($params, 'h4');
|
||||
}
|
||||
|
||||
public static function editorH5(array $params): array
|
||||
{
|
||||
return static::editorHeading($params, 'h5');
|
||||
}
|
||||
|
||||
public static function editorH6(array $params): array
|
||||
{
|
||||
return static::editorHeading($params, 'h6');
|
||||
}
|
||||
|
||||
public static function editorHeading(array $params, string $level): array
|
||||
{
|
||||
return [
|
||||
'content' => [
|
||||
'level' => $level,
|
||||
'text' => $params['content']
|
||||
],
|
||||
'type' => 'heading'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorImage(array $params): array
|
||||
{
|
||||
// internal image
|
||||
if (isset($params['attrs']['id']) === true) {
|
||||
return [
|
||||
'content' => [
|
||||
'alt' => $params['attrs']['alt'] ?? null,
|
||||
'caption' => $params['attrs']['caption'] ?? null,
|
||||
'image' => $params['attrs']['id'] ?? $params['attrs']['src'] ?? null,
|
||||
'location' => 'kirby',
|
||||
'ratio' => $params['attrs']['ratio'] ?? null,
|
||||
],
|
||||
'type' => 'image'
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => [
|
||||
'alt' => $params['attrs']['alt'] ?? null,
|
||||
'caption' => $params['attrs']['caption'] ?? null,
|
||||
'src' => $params['attrs']['src'] ?? null,
|
||||
'location' => 'web',
|
||||
'ratio' => $params['attrs']['ratio'] ?? null,
|
||||
],
|
||||
'type' => 'image'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorKirbytext(array $params): array
|
||||
{
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $params['content']
|
||||
],
|
||||
'type' => 'markdown'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorOl(array $params): array
|
||||
{
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $params['content']
|
||||
],
|
||||
'type' => 'list'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorParagraph(array $params): array
|
||||
{
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $params['content']
|
||||
],
|
||||
'type' => 'text'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorUl(array $params): array
|
||||
{
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $params['content']
|
||||
],
|
||||
'type' => 'list'
|
||||
];
|
||||
}
|
||||
|
||||
public static function editorVideo(array $params): array
|
||||
{
|
||||
return [
|
||||
'content' => [
|
||||
'caption' => $params['attrs']['caption'] ?? null,
|
||||
'url' => $params['attrs']['src'] ?? null
|
||||
],
|
||||
'type' => 'video'
|
||||
];
|
||||
}
|
||||
|
||||
public static function isEditorBlock(array $params): bool
|
||||
{
|
||||
if (isset($params['attrs']) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_string($params['content'] ?? null) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
149
kirby/src/Cms/Blocks.php
Executable file
149
kirby/src/Cms/Blocks.php
Executable file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Data\Json;
|
||||
use Kirby\Data\Yaml;
|
||||
use Kirby\Parsley\Parsley;
|
||||
use Kirby\Parsley\Schema\Blocks as BlockSchema;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* A collection of blocks
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Blocks extends Items
|
||||
{
|
||||
const ITEM_CLASS = '\Kirby\Cms\Block';
|
||||
|
||||
/**
|
||||
* Return HTML when the collection is
|
||||
* converted to a string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toHtml();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the blocks to HTML and then
|
||||
* uses the Str::excerpt method to create
|
||||
* a non-formatted, shortened excerpt from it
|
||||
*
|
||||
* @param mixed ...$args
|
||||
* @return string
|
||||
*/
|
||||
public function excerpt(...$args)
|
||||
{
|
||||
return Str::excerpt($this->toHtml(), ...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around the factory to
|
||||
* catch blocks from layouts
|
||||
*
|
||||
* @param array $items
|
||||
* @param array $params
|
||||
* @return \Kirby\Cms\Blocks
|
||||
*/
|
||||
public static function factory(array $blocks = null, array $params = [])
|
||||
{
|
||||
$blocks = static::extractFromLayouts($blocks);
|
||||
$blocks = BlockConverter::editorBlocks($blocks);
|
||||
|
||||
return parent::factory($blocks, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull out blocks from layouts
|
||||
*
|
||||
* @param array $input
|
||||
* @return array
|
||||
*/
|
||||
protected static function extractFromLayouts(array $input): array
|
||||
{
|
||||
if (empty($input) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// no layouts
|
||||
if (array_key_exists('columns', $input[0]) === false) {
|
||||
return $input;
|
||||
}
|
||||
|
||||
$blocks = [];
|
||||
|
||||
foreach ($input as $layout) {
|
||||
foreach (($layout['columns'] ?? []) as $column) {
|
||||
foreach (($column['blocks'] ?? []) as $block) {
|
||||
$blocks[] = $block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and sanitize various block formats
|
||||
*
|
||||
* @param array|string $input
|
||||
* @return array
|
||||
*/
|
||||
public static function parse($input): array
|
||||
{
|
||||
if (empty($input) === false && is_array($input) === false) {
|
||||
try {
|
||||
$input = Json::decode((string)$input);
|
||||
} catch (Throwable $e) {
|
||||
try {
|
||||
// try to import the old YAML format
|
||||
$yaml = Yaml::decode((string)$input);
|
||||
$first = A::first($yaml);
|
||||
|
||||
// check for valid yaml
|
||||
if (empty($yaml) === true || (isset($first['_key']) === false && isset($first['type']) === false)) {
|
||||
throw new Exception('Invalid YAML');
|
||||
} else {
|
||||
$input = $yaml;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$parser = new Parsley((string)$input, new BlockSchema());
|
||||
$input = $parser->blocks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($input) === true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all blocks to HTML
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function toHtml(): string
|
||||
{
|
||||
$html = [];
|
||||
|
||||
foreach ($this->data as $block) {
|
||||
$html[] = $block->toHtml();
|
||||
}
|
||||
|
||||
return implode($html);
|
||||
}
|
||||
}
|
@@ -201,6 +201,7 @@ class Blueprint
|
||||
|
||||
try {
|
||||
$mixin = static::find($extends);
|
||||
$mixin = static::extend($mixin);
|
||||
$props = A::merge($mixin, $props, A::MERGE_REPLACE);
|
||||
} catch (Exception $e) {
|
||||
// keep the props unextended if the snippet wasn't found
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Collection as BaseCollection;
|
||||
use Kirby\Toolkit\Str;
|
||||
@@ -89,7 +88,7 @@ class Collection extends BaseCollection
|
||||
{
|
||||
if (is_a($object, static::class) === true) {
|
||||
$this->data = array_merge($this->data, $object->data);
|
||||
} elseif (method_exists($object, 'id') === true) {
|
||||
} elseif (is_object($object) === true && method_exists($object, 'id') === true) {
|
||||
$this->__set($object->id(), $object);
|
||||
} else {
|
||||
$this->append($object);
|
||||
@@ -121,45 +120,45 @@ class Collection extends BaseCollection
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the items by a given field. Returns a collection
|
||||
* Groups the items by a given field or callback. Returns a collection
|
||||
* with an item for each group and a collection for each group.
|
||||
*
|
||||
* @param string $field
|
||||
* @param string|Closure $field
|
||||
* @param bool $i Ignore upper/lowercase for group names
|
||||
* @return \Kirby\Cms\Collection
|
||||
* @throws \Kirby\Exception\Exception
|
||||
*/
|
||||
public function groupBy($field, bool $i = true)
|
||||
public function group($field, bool $i = true)
|
||||
{
|
||||
if (is_string($field) === false) {
|
||||
throw new Exception('Cannot group by non-string values. Did you mean to call group()?');
|
||||
if (is_string($field) === true) {
|
||||
$groups = new Collection([], $this->parent());
|
||||
|
||||
foreach ($this->data as $key => $item) {
|
||||
$value = $this->getAttribute($item, $field);
|
||||
|
||||
// make sure that there's always a proper value to group by
|
||||
if (!$value) {
|
||||
throw new InvalidArgumentException('Invalid grouping value for key: ' . $key);
|
||||
}
|
||||
|
||||
// ignore upper/lowercase for group names
|
||||
if ($i) {
|
||||
$value = Str::lower($value);
|
||||
}
|
||||
|
||||
if (isset($groups->data[$value]) === false) {
|
||||
// create a new entry for the group if it does not exist yet
|
||||
$groups->data[$value] = new static([$key => $item]);
|
||||
} else {
|
||||
// add the item to an existing group
|
||||
$groups->data[$value]->set($key, $item);
|
||||
}
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
$groups = new Collection([], $this->parent());
|
||||
|
||||
foreach ($this->data as $key => $item) {
|
||||
$value = $this->getAttribute($item, $field);
|
||||
|
||||
// make sure that there's always a proper value to group by
|
||||
if (!$value) {
|
||||
throw new InvalidArgumentException('Invalid grouping value for key: ' . $key);
|
||||
}
|
||||
|
||||
// ignore upper/lowercase for group names
|
||||
if ($i) {
|
||||
$value = Str::lower($value);
|
||||
}
|
||||
|
||||
if (isset($groups->data[$value]) === false) {
|
||||
// create a new entry for the group if it does not exist yet
|
||||
$groups->data[$value] = new static([$key => $item]);
|
||||
} else {
|
||||
// add the item to an existing group
|
||||
$groups->data[$value]->set($key, $item);
|
||||
}
|
||||
}
|
||||
|
||||
return $groups;
|
||||
return parent::group($field, $i);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,6 +203,7 @@ class Collection extends BaseCollection
|
||||
public function not(...$keys)
|
||||
{
|
||||
$collection = $this->clone();
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (is_array($key) === true) {
|
||||
return $this->not(...$key);
|
||||
@@ -212,8 +212,10 @@ class Collection extends BaseCollection
|
||||
} elseif (is_object($key) === true) {
|
||||
$key = $key->id();
|
||||
}
|
||||
unset($collection->$key);
|
||||
|
||||
unset($collection->{$key});
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
@@ -264,7 +266,7 @@ class Collection extends BaseCollection
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a combination of filterBy, sortBy, not
|
||||
* Runs a combination of filter, sort, not,
|
||||
* offset, limit, search and paginate on the collection.
|
||||
* Any part of the query is optional.
|
||||
*
|
||||
|
@@ -157,7 +157,7 @@ class Event
|
||||
* @param \Closure $hook
|
||||
* @return mixed
|
||||
*/
|
||||
public function call($bind = null, Closure $hook)
|
||||
public function call(?object $bind, Closure $hook)
|
||||
{
|
||||
// collect the list of possible hook arguments
|
||||
$data = $this->arguments();
|
||||
|
@@ -96,7 +96,7 @@ class Field
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function __construct($parent = null, string $key, $value)
|
||||
public function __construct(?object $parent, string $key, $value)
|
||||
{
|
||||
$this->key = $key;
|
||||
$this->value = $value;
|
||||
|
206
kirby/src/Cms/Fieldset.php
Executable file
206
kirby/src/Cms/Fieldset.php
Executable file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Represents a single Fieldset
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Fieldset extends Item
|
||||
{
|
||||
const ITEMS_CLASS = '\Kirby\Cms\Fieldsets';
|
||||
|
||||
protected $disabled;
|
||||
protected $fields = [];
|
||||
protected $icon;
|
||||
protected $label;
|
||||
protected $model;
|
||||
protected $name;
|
||||
protected $preview;
|
||||
protected $tabs;
|
||||
protected $translate;
|
||||
protected $type;
|
||||
protected $unset;
|
||||
protected $wysiwyg;
|
||||
|
||||
/**
|
||||
* Creates a new Fieldset object
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
if (empty($params['type']) === true) {
|
||||
throw new InvalidArgumentException('The fieldset type is missing');
|
||||
}
|
||||
|
||||
$this->type = $params['id'] = $params['type'];
|
||||
|
||||
parent::__construct($params);
|
||||
|
||||
$this->disabled = $params['disabled'] ?? false;
|
||||
$this->icon = $params['icon'] ?? null;
|
||||
$this->model = $this->parent;
|
||||
$this->kirby = $this->parent->kirby();
|
||||
$this->name = $this->createName($params['name'] ?? Str::ucfirst($this->type));
|
||||
$this->label = $this->createLabel($params['label'] ?? null);
|
||||
$this->preview = $params['preview'] ?? null;
|
||||
$this->tabs = $this->createTabs($params);
|
||||
$this->translate = $params['translate'] ?? true;
|
||||
$this->unset = $params['unset'] ?? false;
|
||||
$this->wysiwyg = $params['wysiwyg'] ?? false;
|
||||
|
||||
if (
|
||||
$this->translate === false &&
|
||||
$this->kirby->multilang() === true &&
|
||||
$this->kirby->language()->isDefault() === false
|
||||
) {
|
||||
// disable and unset the fieldset if it's not translatable
|
||||
$this->unset = true;
|
||||
$this->disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function createFields(array $fields = []): array
|
||||
{
|
||||
$fields = Blueprint::fieldsProps($fields);
|
||||
$fields = $this->form($fields)->fields()->toArray();
|
||||
|
||||
// collect all fields
|
||||
$this->fields = array_merge($this->fields, $fields);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
protected function createName($name): string
|
||||
{
|
||||
return I18n::translate($name, $name);
|
||||
}
|
||||
|
||||
protected function createLabel($label = null): ?string
|
||||
{
|
||||
return I18n::translate($label, $label);
|
||||
}
|
||||
|
||||
protected function createTabs(array $params = []): array
|
||||
{
|
||||
$tabs = $params['tabs'] ?? [];
|
||||
|
||||
// return a single tab if there are only fields
|
||||
if (empty($tabs) === true) {
|
||||
return [
|
||||
'content' => [
|
||||
'fields' => $this->createFields($params['fields'] ?? []),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// normalize tabs props
|
||||
foreach ($tabs as $name => $tab) {
|
||||
$tab = Blueprint::extend($tab);
|
||||
|
||||
$tab['fields'] = $this->createFields($tab['fields'] ?? []);
|
||||
$tab['label'] = $this->createLabel($tab['label'] ?? Str::ucfirst($name));
|
||||
$tab['name'] = $name;
|
||||
|
||||
$tabs[$name] = $tab;
|
||||
}
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
public function disabled(): bool
|
||||
{
|
||||
return $this->disabled;
|
||||
}
|
||||
|
||||
public function fields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form for the given fields
|
||||
*
|
||||
* @param array $fields
|
||||
* @param array $input
|
||||
* @return \Kirby\Cms\Form
|
||||
*/
|
||||
public function form(array $fields, array $input = [])
|
||||
{
|
||||
return new Form([
|
||||
'fields' => $fields,
|
||||
'model' => $this->model,
|
||||
'strict' => true,
|
||||
'values' => $input,
|
||||
]);
|
||||
}
|
||||
|
||||
public function icon(): ?string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function label(): ?string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function model()
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function tabs(): array
|
||||
{
|
||||
return $this->tabs;
|
||||
}
|
||||
|
||||
public function translate(): bool
|
||||
{
|
||||
return $this->translate;
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'disabled' => $this->disabled,
|
||||
'icon' => $this->icon,
|
||||
'label' => $this->label,
|
||||
'name' => $this->name,
|
||||
'preview' => $this->preview,
|
||||
'tabs' => $this->tabs,
|
||||
'translate' => $this->translate,
|
||||
'type' => $this->type,
|
||||
'unset' => $this->unset,
|
||||
'wysiwyg' => $this->wysiwyg,
|
||||
];
|
||||
}
|
||||
|
||||
public function unset(): bool
|
||||
{
|
||||
return $this->unset;
|
||||
}
|
||||
}
|
99
kirby/src/Cms/Fieldsets.php
Executable file
99
kirby/src/Cms/Fieldsets.php
Executable file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* A collection of fieldsets
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Fieldsets extends Items
|
||||
{
|
||||
const ITEM_CLASS = '\Kirby\Cms\Fieldset';
|
||||
|
||||
protected static function createFieldsets($params)
|
||||
{
|
||||
$fieldsets = [];
|
||||
$groups = [];
|
||||
|
||||
foreach ($params as $type => $fieldset) {
|
||||
if (is_int($type) === true && is_string($fieldset)) {
|
||||
$type = $fieldset;
|
||||
$fieldset = 'blocks/' . $type;
|
||||
}
|
||||
|
||||
if ($fieldset === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($fieldset === true) {
|
||||
$fieldset = 'blocks/' . $type;
|
||||
}
|
||||
|
||||
$fieldset = Blueprint::extend($fieldset);
|
||||
|
||||
// make sure the type is always set
|
||||
$fieldset['type'] = $fieldset['type'] ?? $type;
|
||||
|
||||
// extract groups
|
||||
if ($fieldset['type'] === 'group') {
|
||||
$result = static::createFieldsets($fieldset['fieldsets'] ?? []);
|
||||
$fieldsets = array_merge($fieldsets, $result['fieldsets']);
|
||||
$label = $fieldset['label'] ?? Str::ucfirst($type);
|
||||
|
||||
$groups[$type] = [
|
||||
'label' => I18n::translate($label, $label),
|
||||
'name' => $type,
|
||||
'open' => $fieldset['open'] ?? true,
|
||||
'sets' => array_column($result['fieldsets'], 'type'),
|
||||
];
|
||||
} else {
|
||||
$fieldsets[$fieldset['type']] = $fieldset;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'fieldsets' => $fieldsets,
|
||||
'groups' => $groups
|
||||
];
|
||||
}
|
||||
|
||||
public static function factory(array $fieldsets = null, array $options = [])
|
||||
{
|
||||
$fieldsets = $fieldsets ?? option('blocks.fieldsets', [
|
||||
'code' => 'blocks/code',
|
||||
'gallery' => 'blocks/gallery',
|
||||
'heading' => 'blocks/heading',
|
||||
'image' => 'blocks/image',
|
||||
'list' => 'blocks/list',
|
||||
'markdown' => 'blocks/markdown',
|
||||
'quote' => 'blocks/quote',
|
||||
'text' => 'blocks/text',
|
||||
'video' => 'blocks/video',
|
||||
]);
|
||||
|
||||
$result = static::createFieldsets($fieldsets);
|
||||
|
||||
return parent::factory($result['fieldsets'], ['groups' => $result['groups']] + $options);
|
||||
}
|
||||
|
||||
public function groups(): array
|
||||
{
|
||||
return $this->options['groups'] ?? [];
|
||||
}
|
||||
|
||||
public function toArray(?Closure $map = null): array
|
||||
{
|
||||
return array_map($map ?? function ($fieldset) {
|
||||
return $fieldset->toArray();
|
||||
}, $this->data);
|
||||
}
|
||||
}
|
@@ -385,19 +385,6 @@ class File extends ModelWithContent
|
||||
return $this->parent()->mediaUrl() . '/' . $this->mediaHash() . '/' . $this->filename();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `File::content()` instead
|
||||
*
|
||||
* @return \Kirby\Cms\Content
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function meta()
|
||||
{
|
||||
deprecated('$file->meta() is deprecated, use $file->content() instead. $file->meta() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->content();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file's last modification time.
|
||||
*
|
||||
@@ -758,7 +745,7 @@ class File extends ModelWithContent
|
||||
*/
|
||||
public function templateSiblings(bool $self = true)
|
||||
{
|
||||
return $this->siblings($self)->filterBy('template', $this->template());
|
||||
return $this->siblings($self)->filter('template', $this->template());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -264,21 +264,6 @@ trait FileActions
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `File::changeName()` instead
|
||||
*
|
||||
* @param string $name
|
||||
* @param bool $sanitize
|
||||
* @return self
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function rename(string $name, bool $sanitize = true)
|
||||
{
|
||||
deprecated('$file->rename() is deprecated, use $file->changeName() instead. $file->rename() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->changeName($name, $sanitize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the file. The source must
|
||||
* be an absolute path to a file or a Url.
|
||||
|
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Extension of the basic blueprint class
|
||||
* to handle all blueprints for files.
|
||||
@@ -14,6 +17,14 @@ namespace Kirby\Cms;
|
||||
*/
|
||||
class FileBlueprint extends Blueprint
|
||||
{
|
||||
/**
|
||||
* `true` if the default accepted
|
||||
* types are being used
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $defaultTypes = false;
|
||||
|
||||
public function __construct(array $props)
|
||||
{
|
||||
parent::__construct($props);
|
||||
@@ -44,6 +55,64 @@ class FileBlueprint extends Blueprint
|
||||
return $this->props['accept'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of all accepted MIME types for
|
||||
* file upload or `*` if all MIME types are allowed
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function acceptMime(): string
|
||||
{
|
||||
// don't disclose the specific default types
|
||||
if ($this->defaultTypes === true) {
|
||||
return '*';
|
||||
}
|
||||
|
||||
$accept = $this->accept();
|
||||
$restrictions = [];
|
||||
|
||||
if (is_array($accept['mime']) === true) {
|
||||
$restrictions[] = $accept['mime'];
|
||||
} else {
|
||||
// only fall back to the extension or type if
|
||||
// no explicit MIME types were defined
|
||||
// (allows to set custom MIME types for the frontend
|
||||
// check but still restrict the extension and/or type)
|
||||
|
||||
if (is_array($accept['extension']) === true) {
|
||||
// determine the main MIME type for each extension
|
||||
$restrictions[] = array_map(['Kirby\Toolkit\Mime', 'fromExtension'], $accept['extension']);
|
||||
}
|
||||
|
||||
if (is_array($accept['type']) === true) {
|
||||
// determine the MIME types of each file type
|
||||
$mimes = [];
|
||||
foreach ($accept['type'] as $type) {
|
||||
if ($extensions = F::typeToExtensions($type)) {
|
||||
$mimes[] = array_map(['Kirby\Toolkit\Mime', 'fromExtension'], $extensions);
|
||||
}
|
||||
}
|
||||
|
||||
$restrictions[] = array_merge(...$mimes);
|
||||
}
|
||||
}
|
||||
|
||||
if ($restrictions !== []) {
|
||||
if (count($restrictions) > 1) {
|
||||
// only return the MIME types that are allowed by all restrictions
|
||||
$mimes = array_intersect(...$restrictions);
|
||||
} else {
|
||||
$mimes = $restrictions[0];
|
||||
}
|
||||
|
||||
// filter out empty MIME types and duplicates
|
||||
return implode(', ', array_filter(array_unique($mimes)));
|
||||
}
|
||||
|
||||
// no restrictions, accept everything
|
||||
return '*';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $accept
|
||||
* @return array
|
||||
@@ -54,16 +123,20 @@ class FileBlueprint extends Blueprint
|
||||
$accept = [
|
||||
'mime' => $accept
|
||||
];
|
||||
}
|
||||
|
||||
// accept anything
|
||||
if (empty($accept) === true) {
|
||||
return [];
|
||||
} elseif ($accept === true) {
|
||||
// explicitly no restrictions at all
|
||||
$accept = [
|
||||
'mime' => null
|
||||
];
|
||||
} elseif (empty($accept) === true) {
|
||||
// no custom restrictions
|
||||
$accept = [];
|
||||
}
|
||||
|
||||
$accept = array_change_key_case($accept);
|
||||
|
||||
$defaults = [
|
||||
'extension' => null,
|
||||
'mime' => null,
|
||||
'maxheight' => null,
|
||||
'maxsize' => null,
|
||||
@@ -71,9 +144,38 @@ class FileBlueprint extends Blueprint
|
||||
'minheight' => null,
|
||||
'minsize' => null,
|
||||
'minwidth' => null,
|
||||
'orientation' => null
|
||||
'orientation' => null,
|
||||
'type' => null
|
||||
];
|
||||
|
||||
return array_merge($defaults, $accept);
|
||||
// default type restriction if none are configured;
|
||||
// this ensures that no unexpected files are uploaded
|
||||
if (
|
||||
array_key_exists('mime', $accept) === false &&
|
||||
array_key_exists('extension', $accept) === false &&
|
||||
array_key_exists('type', $accept) === false
|
||||
) {
|
||||
$defaults['type'] = ['image', 'document', 'archive', 'audio', 'video'];
|
||||
$this->defaultTypes = true;
|
||||
}
|
||||
|
||||
$accept = array_merge($defaults, $accept);
|
||||
|
||||
// normalize the MIME, extension and type from strings into arrays
|
||||
if (is_string($accept['mime']) === true) {
|
||||
$accept['mime'] = array_map(function ($mime) {
|
||||
return $mime['value'];
|
||||
}, Str::accepted($accept['mime']));
|
||||
}
|
||||
|
||||
if (is_string($accept['extension']) === true) {
|
||||
$accept['extension'] = array_map('trim', explode(',', $accept['extension']));
|
||||
}
|
||||
|
||||
if (is_string($accept['type']) === true) {
|
||||
$accept['type'] = array_map('trim', explode(',', $accept['type']));
|
||||
}
|
||||
|
||||
return $accept;
|
||||
}
|
||||
}
|
||||
|
@@ -132,6 +132,14 @@ class Files extends Collection
|
||||
return $this;
|
||||
}
|
||||
|
||||
return $this->filterBy('template', is_array($template) ? 'in' : '==', $template);
|
||||
if ($template === 'default') {
|
||||
$template = ['default', ''];
|
||||
}
|
||||
|
||||
return $this->filter(
|
||||
'template',
|
||||
is_array($template) ? 'in' : '==',
|
||||
$template
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Form\Form as BaseForm;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Extension of `Kirby\Form\Form` that introduces
|
||||
@@ -49,6 +51,42 @@ class Form extends BaseForm
|
||||
parent::__construct($props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the field object by name
|
||||
* and handle nested fields correctly
|
||||
*
|
||||
* @param string $name
|
||||
* @throws \Kirby\Exception\NotFoundException
|
||||
* @return \Kirby\Form\Field
|
||||
*/
|
||||
public function field(string $name)
|
||||
{
|
||||
$form = $this;
|
||||
$fieldNames = Str::split($name, '+');
|
||||
$index = 0;
|
||||
$count = count($fieldNames);
|
||||
$field = null;
|
||||
|
||||
foreach ($fieldNames as $fieldName) {
|
||||
$index++;
|
||||
|
||||
if ($field = $form->fields()->get($fieldName)) {
|
||||
if ($count !== $index) {
|
||||
$form = $field->form();
|
||||
}
|
||||
} else {
|
||||
throw new NotFoundException('The field "' . $fieldName . '" could not be found');
|
||||
}
|
||||
}
|
||||
|
||||
// it can get this error only if $name is an empty string as $name = ''
|
||||
if ($field === null) {
|
||||
throw new NotFoundException('No field could be loaded');
|
||||
}
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Kirby\Cms\Model $model
|
||||
* @param array $props
|
||||
|
@@ -174,18 +174,6 @@ trait HasChildren
|
||||
return $this->drafts()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasUnlistedChildren()` instead
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function hasInvisibleChildren(): bool
|
||||
{
|
||||
deprecated('$page->hasInvisibleChildren() is deprecated, use $page->hasUnlistedChildren() instead. $page->hasInvisibleChildren() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasUnlistedChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page has any listed children
|
||||
*
|
||||
@@ -206,18 +194,6 @@ trait HasChildren
|
||||
return $this->children()->unlisted()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasListedChildren()` instead
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function hasVisibleChildren(): bool
|
||||
{
|
||||
deprecated('$page->hasVisibleChildren() is deprecated, use $page->hasListedChildren() instead. $page->hasVisibleChildren() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasListedChildren();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a flat child index
|
||||
*
|
||||
|
@@ -27,7 +27,7 @@ trait HasFiles
|
||||
*/
|
||||
public function audio()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'audio');
|
||||
return $this->files()->filter('type', '==', 'audio');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ trait HasFiles
|
||||
*/
|
||||
public function code()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'code');
|
||||
return $this->files()->filter('type', '==', 'code');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +74,7 @@ trait HasFiles
|
||||
*/
|
||||
public function documents()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'document');
|
||||
return $this->files()->filter('type', '==', 'document');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +196,7 @@ trait HasFiles
|
||||
*/
|
||||
public function images()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'image');
|
||||
return $this->files()->filter('type', '==', 'image');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,6 +221,6 @@ trait HasFiles
|
||||
*/
|
||||
public function videos()
|
||||
{
|
||||
return $this->files()->filterBy('type', '==', 'video');
|
||||
return $this->files()->filter('type', '==', 'video');
|
||||
}
|
||||
}
|
||||
|
130
kirby/src/Cms/Item.php
Executable file
130
kirby/src/Cms/Item.php
Executable file
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* The Item class is the foundation
|
||||
* for every object in context with
|
||||
* other objects. I.e.
|
||||
*
|
||||
* - a Block in a collection of Blocks
|
||||
* - a Layout in a collection of Layouts
|
||||
* - a Column in a collection of Columns
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Item
|
||||
{
|
||||
const ITEMS_CLASS = '\Kirby\Cms\Items';
|
||||
|
||||
use HasSiblings;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $params;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Items
|
||||
*/
|
||||
protected $siblings;
|
||||
|
||||
/**
|
||||
* Creates a new item
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$siblingsClass = static::ITEMS_CLASS;
|
||||
|
||||
$this->id = $params['id'] ?? uuid();
|
||||
$this->params = $params;
|
||||
$this->parent = $params['parent'] ?? site();
|
||||
$this->siblings = $params['siblings'] ?? new $siblingsClass();
|
||||
}
|
||||
|
||||
/**
|
||||
* Static Item factory
|
||||
*
|
||||
* @param array $params
|
||||
* @return \Kirby\Cms\Item
|
||||
*/
|
||||
public static function factory(array $params)
|
||||
{
|
||||
return new static($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unique item id (UUID v4)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the item to another one
|
||||
*
|
||||
* @param \Kirby\Cms\Item $item
|
||||
* @return bool
|
||||
*/
|
||||
public function is(Item $item): bool
|
||||
{
|
||||
return $this->id() === $item->id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Kirby instance
|
||||
*
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
{
|
||||
return $this->parent()->kirby();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model
|
||||
*
|
||||
* @return \Kirby\Cms\Page | \Kirby\Cms\Site | \Kirby\Cms\File | \Kirby\Cms\User
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sibling collection
|
||||
* This is required by the HasSiblings trait
|
||||
*
|
||||
* @return \Kirby\Editor\Blocks
|
||||
*/
|
||||
protected function siblingsCollection()
|
||||
{
|
||||
return $this->siblings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the item to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id(),
|
||||
];
|
||||
}
|
||||
}
|
96
kirby/src/Cms/Items.php
Executable file
96
kirby/src/Cms/Items.php
Executable file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Closure;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* A collection of items
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Items extends Collection
|
||||
{
|
||||
const ITEM_CLASS = '\Kirby\Cms\Item';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $options;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\ModelWithContent
|
||||
*/
|
||||
protected $parent;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param array $objects
|
||||
* @param array $options
|
||||
*/
|
||||
public function __construct($objects = [], array $options = [])
|
||||
{
|
||||
$this->options = $options;
|
||||
$this->parent = $options['parent'] ?? site();
|
||||
|
||||
parent::__construct($objects, $this->parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new item collection from a
|
||||
* an array of item props
|
||||
*
|
||||
* @param array $items
|
||||
* @param array $params
|
||||
* @return \Kirby\Cms\Items
|
||||
*/
|
||||
public static function factory(array $items = null, array $params = [])
|
||||
{
|
||||
$options = array_merge([
|
||||
'options' => [],
|
||||
'parent' => site(),
|
||||
], $params);
|
||||
|
||||
if (empty($items) === true || is_array($items) === false) {
|
||||
return new static();
|
||||
}
|
||||
|
||||
if (is_array($options) === false) {
|
||||
throw new Exception('Invalid item options');
|
||||
}
|
||||
|
||||
// create a new collection of blocks
|
||||
$collection = new static([], $options);
|
||||
|
||||
foreach ($items as $params) {
|
||||
if (is_array($params) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$params['options'] = $options['options'];
|
||||
$params['parent'] = $options['parent'];
|
||||
$params['siblings'] = $collection;
|
||||
$class = static::ITEM_CLASS;
|
||||
$item = $class::factory($params);
|
||||
$collection->append($item->id(), $item);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the items to an array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(Closure $map = null): array
|
||||
{
|
||||
return array_values(parent::toArray($map));
|
||||
}
|
||||
}
|
@@ -27,7 +27,11 @@ class KirbyTag extends \Kirby\Text\KirbyTag
|
||||
{
|
||||
$parent = $this->parent();
|
||||
|
||||
if (method_exists($parent, 'file') === true && $file = $parent->file($path)) {
|
||||
if (
|
||||
is_object($parent) === true &&
|
||||
method_exists($parent, 'file') === true &&
|
||||
$file = $parent->file($path)
|
||||
) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
|
@@ -4,9 +4,9 @@ namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\PermissionException;
|
||||
use Kirby\Toolkit\F;
|
||||
use Kirby\Toolkit\Locale;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
@@ -328,6 +328,28 @@ class Language extends Model
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the language rules for provided locale code
|
||||
*
|
||||
* @param string $code
|
||||
*/
|
||||
public static function loadRules(string $code)
|
||||
{
|
||||
$kirby = kirby();
|
||||
$code = Str::contains($code, '.') ? Str::before($code, '.') : $code;
|
||||
$file = $kirby->root('i18n:rules') . '/' . $code . '.json';
|
||||
|
||||
if (F::exists($file) === false) {
|
||||
$file = $kirby->root('i18n:rules') . '/' . Str::before($code, '_') . '.json';
|
||||
}
|
||||
|
||||
try {
|
||||
return Data::read($file);
|
||||
} catch (\Exception $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the PHP locale setting array
|
||||
*
|
||||
@@ -343,45 +365,6 @@ class Language extends Model
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale array but with the locale
|
||||
* constants replaced with their string representations
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function localeExport(): array
|
||||
{
|
||||
// list of all possible constant names
|
||||
$constantNames = [
|
||||
'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY',
|
||||
'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'
|
||||
];
|
||||
|
||||
// build an associative array with the locales
|
||||
// that are actually supported on this system
|
||||
$constants = [];
|
||||
foreach ($constantNames as $name) {
|
||||
if (defined($name) === true) {
|
||||
$constants[constant($name)] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// replace the keys in the locale data array with the locale names
|
||||
$return = [];
|
||||
foreach ($this->locale() as $key => $value) {
|
||||
if (isset($constants[$key]) === true) {
|
||||
// the key is a valid constant,
|
||||
// replace it with its string representation
|
||||
$return[$constants[$key]] = $value;
|
||||
} else {
|
||||
// not found, keep it as-is
|
||||
$return[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable name
|
||||
* of the language
|
||||
@@ -454,19 +437,7 @@ class Language extends Model
|
||||
public function rules(): array
|
||||
{
|
||||
$code = $this->locale(LC_CTYPE);
|
||||
$code = Str::contains($code, '.') ? Str::before($code, '.') : $code;
|
||||
$file = $this->kirby()->root('i18n:rules') . '/' . $code . '.json';
|
||||
|
||||
if (F::exists($file) === false) {
|
||||
$file = $this->kirby()->root('i18n:rules') . '/' . Str::before($code, '_') . '.json';
|
||||
}
|
||||
|
||||
try {
|
||||
$data = Data::read($file);
|
||||
} catch (\Exception $e) {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
$data = static::loadRules($code);
|
||||
return array_merge($data, $this->slugs());
|
||||
}
|
||||
|
||||
@@ -488,7 +459,7 @@ class Language extends Model
|
||||
'code' => $this->code(),
|
||||
'default' => $this->isDefault(),
|
||||
'direction' => $this->direction(),
|
||||
'locale' => $this->localeExport(),
|
||||
'locale' => Locale::export($this->locale()),
|
||||
'name' => $this->name(),
|
||||
'translations' => $this->translations(),
|
||||
'url' => $this->url,
|
||||
@@ -539,24 +510,10 @@ class Language extends Model
|
||||
*/
|
||||
protected function setLocale($locale = null)
|
||||
{
|
||||
if (is_array($locale)) {
|
||||
// replace string constant keys with the constant values
|
||||
$convertedLocale = [];
|
||||
foreach ($locale as $key => $value) {
|
||||
if (is_string($key) === true && Str::startsWith($key, 'LC_') === true) {
|
||||
$key = constant($key);
|
||||
}
|
||||
|
||||
$convertedLocale[$key] = $value;
|
||||
}
|
||||
|
||||
$this->locale = $convertedLocale;
|
||||
} elseif (is_string($locale)) {
|
||||
$this->locale = [LC_ALL => $locale];
|
||||
} elseif ($locale === null) {
|
||||
if ($locale === null) {
|
||||
$this->locale = [LC_ALL => $this->code];
|
||||
} else {
|
||||
throw new InvalidArgumentException('Locale must be string or array');
|
||||
$this->locale = Locale::normalize($locale);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
@@ -116,7 +116,7 @@ class LanguageRoutes
|
||||
'action' => function () use ($kirby) {
|
||||
|
||||
// find all languages with the same base url as the current installation
|
||||
$languages = $kirby->languages()->filterBy('baseurl', $kirby->url());
|
||||
$languages = $kirby->languages()->filter('baseurl', $kirby->url());
|
||||
|
||||
// if there's no language with a matching base url,
|
||||
// redirect to the default language
|
||||
|
@@ -72,18 +72,6 @@ class Languages extends Collection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Languages::default()` instead
|
||||
* @return \Kirby\Cms\Language|null
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function findDefault()
|
||||
{
|
||||
deprecated('$languages->findDefault() is deprecated, use $languages->default() instead. $languages->findDefault() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->default();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all defined languages to a collection
|
||||
*
|
||||
|
92
kirby/src/Cms/Layout.php
Executable file
92
kirby/src/Cms/Layout.php
Executable file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* Represents a single Layout with
|
||||
* multiple columns
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Layout extends Item
|
||||
{
|
||||
const ITEMS_CLASS = '\Kirby\Cms\Layouts';
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Content
|
||||
*/
|
||||
protected $attrs;
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\LayoutColumns
|
||||
*/
|
||||
protected $columns;
|
||||
|
||||
/**
|
||||
* Proxy for attrs
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
* @return \Kirby\Cms\Field
|
||||
*/
|
||||
public function __call(string $method, array $args = [])
|
||||
{
|
||||
return $this->attrs()->get($method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Layout object
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
parent::__construct($params);
|
||||
|
||||
$this->columns = LayoutColumns::factory($params['columns'] ?? [], [
|
||||
'parent' => $this->parent
|
||||
]);
|
||||
|
||||
// create the attrs object
|
||||
$this->attrs = new Content($params['attrs'] ?? [], $this->parent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the attrs object
|
||||
*
|
||||
* @return \Kirby\Cms\Content
|
||||
*/
|
||||
public function attrs()
|
||||
{
|
||||
return $this->attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the columns in this layout
|
||||
*
|
||||
* @return \Kirby\Cms\LayoutColumns
|
||||
*/
|
||||
public function columns()
|
||||
{
|
||||
return $this->columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result is being sent to the editor
|
||||
* via the API in the panel
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'attrs' => $this->attrs()->toArray(),
|
||||
'columns' => $this->columns()->toArray(),
|
||||
'id' => $this->id(),
|
||||
];
|
||||
}
|
||||
}
|
96
kirby/src/Cms/LayoutColumn.php
Executable file
96
kirby/src/Cms/LayoutColumn.php
Executable file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Represents a single layout column with
|
||||
* multiple blocks
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class LayoutColumn extends Item
|
||||
{
|
||||
const ITEMS_CLASS = '\Kirby\Cms\LayoutColumns';
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Blocks
|
||||
*/
|
||||
protected $blocks;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $width;
|
||||
|
||||
/**
|
||||
* Creates a new LayoutColumn object
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
parent::__construct($params);
|
||||
|
||||
$this->blocks = Blocks::factory($params['blocks'] ?? [], [
|
||||
'parent' => $this->parent
|
||||
]);
|
||||
|
||||
$this->width = $params['width'] ?? '1/1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the blocks collection
|
||||
*
|
||||
* @return \Kirby\Cms\Blocks
|
||||
*/
|
||||
public function blocks()
|
||||
{
|
||||
return $this->blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of columns this column spans
|
||||
*
|
||||
* @param int $columns
|
||||
* @return int
|
||||
*/
|
||||
public function span(int $columns = 12): int
|
||||
{
|
||||
$fraction = Str::split($this->width, '/');
|
||||
$a = $fraction[0] ?? 1;
|
||||
$b = $fraction[1] ?? 1;
|
||||
|
||||
return $columns * $a / $b;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result is being sent to the editor
|
||||
* via the API in the panel
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'blocks' => $this->blocks()->toArray(),
|
||||
'id' => $this->id(),
|
||||
'width' => $this->width(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the width of the column
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function width(): string
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
}
|
17
kirby/src/Cms/LayoutColumns.php
Executable file
17
kirby/src/Cms/LayoutColumns.php
Executable file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* A collection of layout columns
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class LayoutColumns extends Items
|
||||
{
|
||||
const ITEM_CLASS = '\Kirby\Cms\LayoutColumn';
|
||||
}
|
39
kirby/src/Cms/Layouts.php
Executable file
39
kirby/src/Cms/Layouts.php
Executable file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Cms;
|
||||
|
||||
/**
|
||||
* A collection of layouts
|
||||
*
|
||||
* @package Kirby Cms
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Layouts extends Items
|
||||
{
|
||||
const ITEM_CLASS = '\Kirby\Cms\Layout';
|
||||
|
||||
public static function factory(array $layouts = null, array $options = [])
|
||||
{
|
||||
$first = $layouts[0] ?? [];
|
||||
|
||||
// if there are no wrapping layouts for blocks yet …
|
||||
if (array_key_exists('content', $first) === true || array_key_exists('type', $first) === true) {
|
||||
$layouts = [
|
||||
[
|
||||
'id' => uuid(),
|
||||
'columns' => [
|
||||
[
|
||||
'width' => '1/1',
|
||||
'blocks' => $layouts
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return parent::factory($layouts, $options);
|
||||
}
|
||||
}
|
@@ -294,9 +294,7 @@ abstract class ModelWithContent extends Model
|
||||
$errors = [];
|
||||
|
||||
foreach ($this->blueprint()->sections() as $section) {
|
||||
if (method_exists($section, 'errors') === true || isset($section->errors)) {
|
||||
$errors = array_merge($errors, $section->errors());
|
||||
}
|
||||
$errors = array_merge($errors, $section->errors());
|
||||
}
|
||||
|
||||
return $errors;
|
||||
|
@@ -753,18 +753,6 @@ class Page extends ModelWithContent
|
||||
return $this->isHomePage() === true || $this->isErrorPage() === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::isUnlisted()` instead
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function isInvisible(): bool
|
||||
{
|
||||
deprecated('$page->isInvisible() is deprecated, use $page->isUnlisted() instead. $page->isInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->isUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page has a sorting number
|
||||
*
|
||||
@@ -845,18 +833,6 @@ class Page extends ModelWithContent
|
||||
return $this->isListed() === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::isListed()` instead
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function isVisible(): bool
|
||||
{
|
||||
deprecated('$page->isVisible() is deprecated, use $page->isListed() instead. $page->isVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->isListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page access is verified.
|
||||
* This is only used for drafts so far.
|
||||
|
@@ -23,9 +23,11 @@ use Kirby\Toolkit\Str;
|
||||
trait PageActions
|
||||
{
|
||||
/**
|
||||
* Changes the sorting number
|
||||
* Changes the sorting number.
|
||||
* The sorting number must already be correct
|
||||
* when the method is called
|
||||
* when the method is called.
|
||||
* This only affects this page,
|
||||
* siblings will not be resorted.
|
||||
*
|
||||
* @param int|null $num
|
||||
* @return self
|
||||
@@ -168,7 +170,9 @@ trait PageActions
|
||||
|
||||
/**
|
||||
* Change the status of the current page
|
||||
* to either draft, listed or unlisted
|
||||
* to either draft, listed or unlisted.
|
||||
* If changing to `listed`, you can pass a position for the
|
||||
* page in the siblings collection. Siblings will be resorted.
|
||||
*
|
||||
* @param string $status "draft", "listed" or "unlisted"
|
||||
* @param int|null $position Optional sorting number
|
||||
@@ -203,7 +207,7 @@ trait PageActions
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $position
|
||||
* @param int|null $position
|
||||
* @return self
|
||||
*/
|
||||
protected function changeStatusToListed(int $position = null)
|
||||
@@ -247,6 +251,19 @@ trait PageActions
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the position of the page in its siblings
|
||||
* collection. Siblings will be resorted. If the page
|
||||
* status isn't yet `listed`, it will be changed to it.
|
||||
*
|
||||
* @param int|null $position
|
||||
* @return self
|
||||
*/
|
||||
public function changeSort(int $position = null)
|
||||
{
|
||||
return $this->changeStatus('listed', $position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the page template
|
||||
*
|
||||
@@ -545,7 +562,7 @@ trait PageActions
|
||||
return $num;
|
||||
default:
|
||||
// get instance with default language
|
||||
$app = $this->kirby()->clone();
|
||||
$app = $this->kirby()->clone([], false);
|
||||
$app->setCurrentLanguage();
|
||||
|
||||
$template = Str::template($mode, [
|
||||
@@ -735,7 +752,7 @@ trait PageActions
|
||||
}
|
||||
|
||||
$parent = $this->parentModel();
|
||||
$parent->children = $parent->children()->sortBy('num', 'asc');
|
||||
$parent->children = $parent->children()->sort('num', 'asc');
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -761,18 +778,22 @@ trait PageActions
|
||||
$sibling->changeNum($index);
|
||||
}
|
||||
|
||||
$parent->children = $siblings->sortBy('num', 'asc');
|
||||
$parent->children = $siblings->sort('num', 'asc');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.5.0 Use `Page::changeSort()` instead
|
||||
*
|
||||
* @param null $position
|
||||
* @return self
|
||||
*/
|
||||
public function sort($position = null)
|
||||
{
|
||||
deprecated('$page->sort() is deprecated, use $page->changeSort() instead. $page->sort() will be removed in Kirby 3.6.0.');
|
||||
|
||||
return $this->changeStatus('listed', $position);
|
||||
}
|
||||
|
||||
|
@@ -162,7 +162,7 @@ class PagePicker extends Picker
|
||||
}
|
||||
|
||||
// filter protected pages
|
||||
$items = $items->filterBy('isReadable', true);
|
||||
$items = $items->filter('isReadable', true);
|
||||
|
||||
// search
|
||||
$items = $this->search($items);
|
||||
|
@@ -135,103 +135,6 @@ trait PageSiblings
|
||||
*/
|
||||
public function templateSiblings(bool $self = true)
|
||||
{
|
||||
return $this->siblings($self)->filterBy('intendedTemplate', $this->intendedTemplate()->name());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasNextUnlisted()` instead
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function hasNextInvisible(): bool
|
||||
{
|
||||
deprecated('$page->hasNextInvisible() is deprecated, use $page->hasNextUnlisted() instead. $page->hasNextInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasNextUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasNextListed()` instead
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function hasNextVisible(): bool
|
||||
{
|
||||
deprecated('$page->hasNextVisible() is deprecated, use $page->hasNextListed() instead. $page->hasNextVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasNextListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasPrevUnlisted()` instead
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function hasPrevInvisible(): bool
|
||||
{
|
||||
deprecated('$page->hasPrevInvisible() is deprecated, use $page->hasPrevUnlisted() instead. $page->hasPrevInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasPrevUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::hasPrevListed()` instead
|
||||
* @return bool
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function hasPrevVisible(): bool
|
||||
{
|
||||
deprecated('$page->hasPrevVisible() is deprecated, use $page->hasPrevListed() instead. $page->hasPrevVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->hasPrevListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::nextUnlisted()` instead
|
||||
* @return self|null
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function nextInvisible()
|
||||
{
|
||||
deprecated('$page->nextInvisible() is deprecated, use $page->nextUnlisted() instead. $page->nextInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->nextUnlisted();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::nextListed()` instead
|
||||
* @return self|null
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function nextVisible()
|
||||
{
|
||||
deprecated('$page->nextVisible() is deprecated, use $page->nextListed() instead. $page->nextVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->nextListed();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::prevUnlisted()` instead
|
||||
* @return self|null
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function prevInvisible()
|
||||
{
|
||||
deprecated('$page->prevInvisible() is deprecated, use $page->prevUnlisted() instead. $page->prevInvisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->prevUnlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Page::prevListed()` instead
|
||||
* @return self|null
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function prevVisible()
|
||||
{
|
||||
deprecated('$page->prevVisible() is deprecated, use $page->prevListed() instead. $page->prevVisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->prevListed();
|
||||
return $this->siblings($self)->filter('intendedTemplate', $this->intendedTemplate()->name());
|
||||
}
|
||||
}
|
||||
|
@@ -74,7 +74,7 @@ class Pages extends Collection
|
||||
*/
|
||||
public function audio()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'audio');
|
||||
return $this->files()->filter('type', 'audio');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,7 +102,7 @@ class Pages extends Collection
|
||||
*/
|
||||
public function code()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'code');
|
||||
return $this->files()->filter('type', 'code');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +112,7 @@ class Pages extends Collection
|
||||
*/
|
||||
public function documents()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'document');
|
||||
return $this->files()->filter('type', 'document');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,7 +319,7 @@ class Pages extends Collection
|
||||
*/
|
||||
public function images()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'image');
|
||||
return $this->files()->filter('type', 'image');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,19 +351,6 @@ class Pages extends Collection
|
||||
return $this->index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Pages::unlisted()` instead
|
||||
*
|
||||
* @return self
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function invisible()
|
||||
{
|
||||
deprecated('$pages->invisible() is deprecated, use $pages->unlisted() instead. $pages->invisible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->unlisted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all listed pages in the collection
|
||||
*
|
||||
@@ -371,7 +358,7 @@ class Pages extends Collection
|
||||
*/
|
||||
public function listed()
|
||||
{
|
||||
return $this->filterBy('isListed', '==', true);
|
||||
return $this->filter('isListed', '==', true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,7 +368,7 @@ class Pages extends Collection
|
||||
*/
|
||||
public function unlisted()
|
||||
{
|
||||
return $this->filterBy('isUnlisted', '==', true);
|
||||
return $this->filter('isUnlisted', '==', true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,7 +465,7 @@ class Pages extends Collection
|
||||
*/
|
||||
public function published()
|
||||
{
|
||||
return $this->filterBy('isDraft', '==', false);
|
||||
return $this->filter('isDraft', '==', false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -509,19 +496,6 @@ class Pages extends Collection
|
||||
*/
|
||||
public function videos()
|
||||
{
|
||||
return $this->files()->filterBy('type', 'video');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 3.0.0 Use `Pages::listed()` instead
|
||||
*
|
||||
* @return \Kirby\Cms\Pages
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function visible()
|
||||
{
|
||||
deprecated('$pages->visible() is deprecated, use $pages->listed() instead. $pages->visible() will be removed in Kirby 3.5.0.');
|
||||
|
||||
return $this->listed();
|
||||
return $this->files()->filter('type', 'video');
|
||||
}
|
||||
}
|
||||
|
@@ -140,7 +140,7 @@ class Plugin extends Model
|
||||
*/
|
||||
protected function setName(string $name)
|
||||
{
|
||||
if (preg_match('!^[a-z0-9-]+\/[a-z0-9-]+$!i', $name) == false) {
|
||||
if (preg_match('!^[a-z0-9-]+\/[a-z0-9-]+$!i', $name) !== 1) {
|
||||
throw new InvalidArgumentException('The plugin name must follow the format "a-z0-9-/a-z0-9-"');
|
||||
}
|
||||
|
||||
|
@@ -87,7 +87,7 @@ class Roles extends Collection
|
||||
}
|
||||
|
||||
// return the collection sorted by name
|
||||
return $collection->sortBy('name', 'asc');
|
||||
return $collection->sort('name', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +134,6 @@ class Roles extends Collection
|
||||
}
|
||||
|
||||
// return the collection sorted by name
|
||||
return $roles->sortBy('name', 'asc');
|
||||
return $roles->sort('name', 'asc');
|
||||
}
|
||||
}
|
||||
|
@@ -51,6 +51,15 @@ class Section extends Component
|
||||
parent::__construct($type, $attrs);
|
||||
}
|
||||
|
||||
public function errors(): array
|
||||
{
|
||||
if (array_key_exists('errors', $this->methods) === true) {
|
||||
return $this->methods['errors']->call($this);
|
||||
}
|
||||
|
||||
return $this->errors ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
|
@@ -340,6 +340,68 @@ class System
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured UI modes for the login form
|
||||
* with their respective options
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \Kirby\Exception\InvalidArgumentException If the configuration is invalid
|
||||
* (only in debug mode)
|
||||
*/
|
||||
public function loginMethods(): array
|
||||
{
|
||||
$default = ['password' => []];
|
||||
$methods = A::wrap($this->app->option('auth.methods', $default));
|
||||
|
||||
// normalize the syntax variants
|
||||
$normalized = [];
|
||||
$uses2fa = false;
|
||||
foreach ($methods as $key => $value) {
|
||||
if (is_int($key) === true) {
|
||||
// ['password']
|
||||
$normalized[$value] = [];
|
||||
} elseif ($value === true) {
|
||||
// ['password' => true]
|
||||
$normalized[$key] = [];
|
||||
} else {
|
||||
// ['password' => [...]]
|
||||
$normalized[$key] = $value;
|
||||
|
||||
if (isset($value['2fa']) === true && $value['2fa'] === true) {
|
||||
$uses2fa = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2FA must not be circumvented by code-based modes
|
||||
foreach (['code', 'password-reset'] as $method) {
|
||||
if ($uses2fa === true && isset($normalized[$method]) === true) {
|
||||
unset($normalized[$method]);
|
||||
|
||||
if ($this->app->option('debug') === true) {
|
||||
$message = 'The "' . $method . '" login method cannot be enabled when 2FA is required';
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// only one code-based mode can be active at once
|
||||
if (
|
||||
isset($normalized['code']) === true &&
|
||||
isset($normalized['password-reset']) === true
|
||||
) {
|
||||
unset($normalized['code']);
|
||||
|
||||
if ($this->app->option('debug') === true) {
|
||||
$message = 'The "code" and "password-reset" login methods cannot be enabled together';
|
||||
throw new InvalidArgumentException($message);
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for an existing mbstring extension
|
||||
*
|
||||
@@ -367,7 +429,9 @@ class System
|
||||
*/
|
||||
public function php(): bool
|
||||
{
|
||||
return version_compare(phpversion(), '7.1.0', '>=');
|
||||
return
|
||||
version_compare(PHP_VERSION, '7.3.0', '>=') === true &&
|
||||
version_compare(PHP_VERSION, '8.1.0', '<') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -368,7 +368,8 @@ class User extends ModelWithContent
|
||||
*/
|
||||
public function isLastAdmin(): bool
|
||||
{
|
||||
return $this->role()->isAdmin() === true && $this->kirby()->users()->filterBy('role', 'admin')->count() <= 1;
|
||||
return $this->role()->isAdmin() === true &&
|
||||
$this->kirby()->users()->filter('role', 'admin')->count() <= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,6 +382,17 @@ class User extends ModelWithContent
|
||||
return $this->kirby()->users()->count() === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current user is the virtual
|
||||
* Nobody user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isNobody(): bool
|
||||
{
|
||||
return $this->email() === 'nobody@getkirby.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user language
|
||||
*
|
||||
@@ -421,7 +433,7 @@ class User extends ModelWithContent
|
||||
$kirby->trigger('user.login:before', ['user' => $this, 'session' => $session]);
|
||||
|
||||
$session->regenerateToken(); // privilege change
|
||||
$session->data()->set('user.id', $this->id());
|
||||
$session->data()->set('kirby.userId', $this->id());
|
||||
$this->kirby()->auth()->setUser($this);
|
||||
|
||||
$kirby->trigger('user.login:after', ['user' => $this, 'session' => $session]);
|
||||
@@ -441,7 +453,7 @@ class User extends ModelWithContent
|
||||
$kirby->trigger('user.logout:before', ['user' => $this, 'session' => $session]);
|
||||
|
||||
// remove the user from the session for future requests
|
||||
$session->data()->remove('user.id');
|
||||
$session->data()->remove('kirby.userId');
|
||||
|
||||
// clear the cached user object from the app state of the current request
|
||||
$this->kirby()->auth()->flush();
|
||||
@@ -538,6 +550,18 @@ class User extends ModelWithContent
|
||||
return $this->name = new Field($this, 'name', $this->credentials()['name'] ?? null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's name or,
|
||||
* if empty, the email address
|
||||
*
|
||||
* @return \Kirby\Cms\Field
|
||||
*/
|
||||
public function nameOrEmail()
|
||||
{
|
||||
$name = $this->name();
|
||||
return $name->isNotEmpty() ? $name : new Field($this, 'email', $this->email());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dummy nobody
|
||||
*
|
||||
@@ -688,7 +712,7 @@ class User extends ModelWithContent
|
||||
$roles = $kirby->roles();
|
||||
|
||||
// a collection with just the one role of the user
|
||||
$myRole = $roles->filterBy('id', $this->role()->id());
|
||||
$myRole = $roles->filter('id', $this->role()->id());
|
||||
|
||||
// if there's an authenticated user …
|
||||
if ($user = $kirby->user()) {
|
||||
|
@@ -140,7 +140,7 @@ trait UserActions
|
||||
* @return mixed
|
||||
* @throws \Kirby\Exception\PermissionException
|
||||
*/
|
||||
protected function commit(string $action, array $arguments = [], Closure $callback)
|
||||
protected function commit(string $action, array $arguments, Closure $callback)
|
||||
{
|
||||
if ($this->isKirby() === true) {
|
||||
throw new PermissionException('The Kirby user cannot be changed');
|
||||
|
@@ -50,7 +50,7 @@ class UserPermissions extends ModelPermissions
|
||||
}
|
||||
|
||||
// users who are not admins cannot create admins
|
||||
if ($this->model->isAdmin() === false) {
|
||||
if ($this->model->isAdmin() === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@@ -61,7 +61,7 @@ class UserPicker extends Picker
|
||||
$users = $this->search($users);
|
||||
|
||||
// sort
|
||||
$users = $users->sortBy('username', 'asc');
|
||||
$users = $users->sort('username', 'asc');
|
||||
|
||||
// paginate
|
||||
return $this->paginate($users);
|
||||
|
@@ -165,6 +165,12 @@ class UserRules
|
||||
static::validEmail($user, $user->email(), true);
|
||||
static::validLanguage($user, $user->language());
|
||||
|
||||
// the first user must have a password
|
||||
if ($user->kirby()->users()->count() === 0 && empty($props['password'])) {
|
||||
// trigger invalid password error
|
||||
static::validPassword($user, ' ');
|
||||
}
|
||||
|
||||
if (empty($props['password']) === false) {
|
||||
static::validPassword($user, $props['password']);
|
||||
}
|
||||
|
@@ -128,13 +128,13 @@ class Users extends Collection
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for `$users->filterBy('role', 'admin')`
|
||||
* Shortcut for `$users->filter('role', 'admin')`
|
||||
*
|
||||
* @param string $role
|
||||
* @return self
|
||||
*/
|
||||
public function role(string $role)
|
||||
{
|
||||
return $this->filterBy('role', $role);
|
||||
return $this->filter('role', $role);
|
||||
}
|
||||
}
|
||||
|
@@ -79,7 +79,7 @@ class Data
|
||||
* @param string $type
|
||||
* @return array
|
||||
*/
|
||||
public static function decode($string = null, string $type): array
|
||||
public static function decode($string, string $type): array
|
||||
{
|
||||
return static::handler($type)->decode($string);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class Data
|
||||
* @param string $type
|
||||
* @return string
|
||||
*/
|
||||
public static function encode($data = null, string $type): string
|
||||
public static function encode($data, string $type): string
|
||||
{
|
||||
return static::handler($type)->encode($data);
|
||||
}
|
||||
|
@@ -24,6 +24,9 @@ class Yaml extends Handler
|
||||
*/
|
||||
public static function encode($data): string
|
||||
{
|
||||
// TODO: The locale magic should no longer be
|
||||
// necessary when support for PHP 7.x is dropped
|
||||
|
||||
// fetch the current locale setting for numbers
|
||||
$locale = setlocale(LC_NUMERIC, 0);
|
||||
|
||||
|
@@ -363,7 +363,7 @@ abstract class Sql
|
||||
* @param array $input
|
||||
* @return void
|
||||
*/
|
||||
public function extend(&$query, array &$bindings = [], $input)
|
||||
public function extend(&$query, array &$bindings, $input)
|
||||
{
|
||||
if (empty($input['query']) === false) {
|
||||
$query[] = $input['query'];
|
||||
|
@@ -21,6 +21,22 @@ class Email
|
||||
{
|
||||
use Properties;
|
||||
|
||||
/**
|
||||
* If set to `true`, the debug mode is enabled
|
||||
* for all emails
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public static $debug = false;
|
||||
|
||||
/**
|
||||
* Store for sent emails when `Email::$debug`
|
||||
* is set to `true`
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $emails = [];
|
||||
|
||||
/**
|
||||
* @var array|null
|
||||
*/
|
||||
@@ -96,9 +112,13 @@ class Email
|
||||
{
|
||||
$this->setProperties($props);
|
||||
|
||||
if ($debug === false) {
|
||||
$this->send(); // @codeCoverageIgnore
|
||||
// @codeCoverageIgnoreStart
|
||||
if (static::$debug === false && $debug === false) {
|
||||
$this->send();
|
||||
} elseif (static::$debug === true) {
|
||||
static::$emails[] = $this;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -248,6 +248,26 @@ class Field extends Component
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new field instance
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $attrs
|
||||
* @param Fields|null $formFields
|
||||
* @return static
|
||||
*/
|
||||
public static function factory(string $type, array $attrs = [], ?Fields $formFields = null)
|
||||
{
|
||||
$field = static::$types[$type] ?? null;
|
||||
|
||||
if (is_string($field) && class_exists($field) === true) {
|
||||
$attrs['siblings'] = $formFields;
|
||||
return new $field($attrs);
|
||||
}
|
||||
|
||||
return new static($type, $attrs, $formFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent collection with all fields of the current form
|
||||
*
|
||||
@@ -404,8 +424,6 @@ class Field extends Component
|
||||
|
||||
unset($array['model']);
|
||||
|
||||
$array['errors'] = $this->errors();
|
||||
$array['invalid'] = $this->isInvalid();
|
||||
$array['saveable'] = $this->save();
|
||||
$array['signature'] = md5(json_encode($array));
|
||||
|
||||
|
265
kirby/src/Form/Field/BlocksField.php
Executable file
265
kirby/src/Form/Field/BlocksField.php
Executable file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Form\Field;
|
||||
|
||||
use Kirby\Cms\Block;
|
||||
use Kirby\Cms\Blocks as BlocksCollection;
|
||||
use Kirby\Cms\Fieldsets;
|
||||
use Kirby\Cms\Form;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Form\FieldClass;
|
||||
use Kirby\Form\Mixin\EmptyState;
|
||||
use Kirby\Form\Mixin\Max;
|
||||
use Kirby\Form\Mixin\Min;
|
||||
use Throwable;
|
||||
|
||||
class BlocksField extends FieldClass
|
||||
{
|
||||
use EmptyState;
|
||||
use Max;
|
||||
use Min;
|
||||
|
||||
protected $fieldsets;
|
||||
protected $blocks;
|
||||
protected $value = [];
|
||||
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->setFieldsets($params['fieldsets'] ?? null, $params['model'] ?? site());
|
||||
|
||||
parent::__construct($params);
|
||||
|
||||
$this->setEmpty($params['empty'] ?? null);
|
||||
$this->setGroup($params['group'] ?? 'blocks');
|
||||
$this->setMax($params['max'] ?? null);
|
||||
$this->setMin($params['min'] ?? null);
|
||||
$this->setPretty($params['pretty'] ?? false);
|
||||
}
|
||||
|
||||
public function blocksToValues($blocks, $to = 'values'): array
|
||||
{
|
||||
$result = [];
|
||||
$fields = [];
|
||||
|
||||
foreach ($blocks as $block) {
|
||||
try {
|
||||
$type = $block['type'];
|
||||
|
||||
// get and cache fields at the same time
|
||||
$fields[$type] = $fields[$type] ?? $this->fields($block['type']);
|
||||
|
||||
// overwrite the block content with form values
|
||||
$block['content'] = $this->form($fields[$type], $block['content'])->$to();
|
||||
|
||||
$result[] = $block;
|
||||
} catch (Throwable $e) {
|
||||
$result[] = $block;
|
||||
|
||||
// skip invalid blocks
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function fields(string $type)
|
||||
{
|
||||
return $this->fieldset($type)->fields();
|
||||
}
|
||||
|
||||
public function fieldset(string $type)
|
||||
{
|
||||
if ($fieldset = $this->fieldsets->find($type)) {
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
throw new NotFoundException('The fieldset ' . $type . ' could not be found');
|
||||
}
|
||||
|
||||
public function fieldsets()
|
||||
{
|
||||
return $this->fieldsets;
|
||||
}
|
||||
|
||||
public function fieldsetGroups(): ?array
|
||||
{
|
||||
$fieldsetGroups = $this->fieldsets()->groups();
|
||||
return empty($fieldsetGroups) === true ? null : $fieldsetGroups;
|
||||
}
|
||||
|
||||
public function fill($value = null)
|
||||
{
|
||||
$value = BlocksCollection::parse($value);
|
||||
$blocks = BlocksCollection::factory($value);
|
||||
$this->value = $this->blocksToValues($blocks->toArray());
|
||||
}
|
||||
|
||||
public function form(array $fields, array $input = [])
|
||||
{
|
||||
return new Form([
|
||||
'fields' => $fields,
|
||||
'model' => $this->model,
|
||||
'strict' => true,
|
||||
'values' => $input,
|
||||
]);
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return count($this->value()) === 0;
|
||||
}
|
||||
|
||||
public function group(): string
|
||||
{
|
||||
return $this->group;
|
||||
}
|
||||
|
||||
public function pretty(): bool
|
||||
{
|
||||
return $this->pretty;
|
||||
}
|
||||
|
||||
public function props(): array
|
||||
{
|
||||
return [
|
||||
'empty' => $this->empty(),
|
||||
'fieldsets' => $this->fieldsets()->toArray(),
|
||||
'fieldsetGroups' => $this->fieldsetGroups(),
|
||||
'group' => $this->group(),
|
||||
'max' => $this->max(),
|
||||
'min' => $this->min(),
|
||||
] + parent::props();
|
||||
}
|
||||
|
||||
public function routes(): array
|
||||
{
|
||||
$field = $this;
|
||||
|
||||
return [
|
||||
[
|
||||
'pattern' => 'uuid',
|
||||
'action' => function () {
|
||||
return ['uuid' => uuid()];
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'fieldsets/(:any)',
|
||||
'method' => 'GET',
|
||||
'action' => function ($fieldsetType) use ($field) {
|
||||
$fields = $field->fields($fieldsetType);
|
||||
$defaults = $field->form($fields, [])->data(true);
|
||||
$content = $field->form($fields, $defaults)->values();
|
||||
|
||||
return Block::factory([
|
||||
'content' => $content,
|
||||
'type' => $fieldsetType
|
||||
])->toArray();
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'fieldsets/(:any)/fields/(:any)/(:all?)',
|
||||
'method' => 'ALL',
|
||||
'action' => function (string $fieldsetType, string $fieldName, string $path = null) use ($field) {
|
||||
$fields = $field->fields($fieldsetType);
|
||||
$field = $field->form($fields)->field($fieldName);
|
||||
|
||||
$fieldApi = $this->clone([
|
||||
'routes' => $field->api(),
|
||||
'data' => array_merge($this->data(), ['field' => $field])
|
||||
]);
|
||||
|
||||
return $fieldApi->call($path, $this->requestMethod(), $this->requestData());
|
||||
}
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function store($value)
|
||||
{
|
||||
$blocks = $this->blocksToValues((array)$value, 'content');
|
||||
return $this->valueToJson($blocks, $this->pretty());
|
||||
}
|
||||
|
||||
protected function setFieldsets($fieldsets, $model)
|
||||
{
|
||||
if (is_string($fieldsets) === true) {
|
||||
$fieldsets = [];
|
||||
}
|
||||
|
||||
$this->fieldsets = Fieldsets::factory($fieldsets, [
|
||||
'parent' => $model
|
||||
]);
|
||||
}
|
||||
|
||||
protected function setGroup(string $group = null)
|
||||
{
|
||||
$this->group = $group;
|
||||
}
|
||||
|
||||
protected function setPretty(bool $pretty = false)
|
||||
{
|
||||
$this->pretty = $pretty;
|
||||
}
|
||||
|
||||
public function validations(): array
|
||||
{
|
||||
return [
|
||||
'blocks' => function ($value) {
|
||||
if ($this->min && count($value) < $this->min) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'blocks.min.' . ($this->min === 1 ? 'singular' : 'plural'),
|
||||
'data' => [
|
||||
'min' => $this->min
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
if ($this->max && count($value) > $this->max) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'blocks.max.' . ($this->max === 1 ? 'singular' : 'plural'),
|
||||
'data' => [
|
||||
'max' => $this->max
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
$fields = [];
|
||||
$index = 0;
|
||||
|
||||
foreach ($value as $block) {
|
||||
$index++;
|
||||
$blockType = $block['type'];
|
||||
|
||||
try {
|
||||
$blockFields = $fields[$blockType] ?? $this->fields($blockType) ?? [];
|
||||
} catch (Throwable $e) {
|
||||
// skip invalid blocks
|
||||
continue;
|
||||
}
|
||||
|
||||
// store the fields for the next round
|
||||
$fields[$blockType] = $blockFields;
|
||||
|
||||
// overwrite the content with the serialized form
|
||||
foreach ($this->form($blockFields, $block['content'])->fields() as $field) {
|
||||
$errors = $field->errors();
|
||||
|
||||
// rough first validation
|
||||
if (empty($errors) === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'blocks.validation',
|
||||
'data' => [
|
||||
'index' => $index,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
224
kirby/src/Form/Field/LayoutField.php
Executable file
224
kirby/src/Form/Field/LayoutField.php
Executable file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Form\Field;
|
||||
|
||||
use Kirby\Cms\Fieldset;
|
||||
use Kirby\Cms\Form;
|
||||
use Kirby\Cms\Layout;
|
||||
use Kirby\Cms\Layouts;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
class LayoutField extends BlocksField
|
||||
{
|
||||
protected $layouts;
|
||||
protected $settings;
|
||||
|
||||
public function __construct(array $params)
|
||||
{
|
||||
$this->setModel($params['model'] ?? site());
|
||||
$this->setLayouts($params['layouts'] ?? ['1/1']);
|
||||
$this->setSettings($params['settings'] ?? []);
|
||||
|
||||
parent::__construct($params);
|
||||
}
|
||||
|
||||
public function fill($value = null)
|
||||
{
|
||||
$value = $this->valueFromJson($value);
|
||||
$layouts = Layouts::factory($value, ['parent' => $this->model])->toArray();
|
||||
|
||||
foreach ($layouts as $layoutIndex => $layout) {
|
||||
if ($this->settings !== null) {
|
||||
$layouts[$layoutIndex]['attrs'] = $this->attrsForm($layout['attrs'])->values();
|
||||
}
|
||||
|
||||
foreach ($layout['columns'] as $columnIndex => $column) {
|
||||
$layouts[$layoutIndex]['columns'][$columnIndex]['blocks'] = $this->blocksToValues($column['blocks']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->value = $layouts;
|
||||
}
|
||||
|
||||
public function attrsForm(array $input = [])
|
||||
{
|
||||
$settings = $this->settings();
|
||||
|
||||
return new Form([
|
||||
'fields' => $settings ? $settings->fields() : [],
|
||||
'model' => $this->model,
|
||||
'strict' => true,
|
||||
'values' => $input,
|
||||
]);
|
||||
}
|
||||
|
||||
public function layouts(): ?array
|
||||
{
|
||||
return $this->layouts;
|
||||
}
|
||||
|
||||
public function props(): array
|
||||
{
|
||||
$settings = $this->settings();
|
||||
|
||||
return array_merge(parent::props(), [
|
||||
'settings' => $settings !== null ? $settings->toArray() : null,
|
||||
'layouts' => $this->layouts()
|
||||
]);
|
||||
}
|
||||
|
||||
public function routes(): array
|
||||
{
|
||||
$field = $this;
|
||||
$routes = parent::routes();
|
||||
$routes[] = [
|
||||
'pattern' => 'layout',
|
||||
'method' => 'POST',
|
||||
'action' => function () use ($field) {
|
||||
$defaults = $field->attrsForm([])->data(true);
|
||||
$attrs = $field->attrsForm($defaults)->values();
|
||||
$columns = get('columns') ?? ['1/1'];
|
||||
|
||||
return Layout::factory([
|
||||
'attrs' => $attrs,
|
||||
'columns' => array_map(function ($width) {
|
||||
return [
|
||||
'blocks' => [],
|
||||
'id' => uuid(),
|
||||
'width' => $width,
|
||||
];
|
||||
}, $columns)
|
||||
])->toArray();
|
||||
},
|
||||
];
|
||||
|
||||
$routes[] = [
|
||||
'pattern' => 'fields/(:any)/(:all?)',
|
||||
'method' => 'ALL',
|
||||
'action' => function (string $fieldName, string $path = null) use ($field) {
|
||||
$form = $field->attrsForm();
|
||||
$field = $form->field($fieldName);
|
||||
|
||||
$fieldApi = $this->clone([
|
||||
'routes' => $field->api(),
|
||||
'data' => array_merge($this->data(), ['field' => $field])
|
||||
]);
|
||||
|
||||
return $fieldApi->call($path, $this->requestMethod(), $this->requestData());
|
||||
}
|
||||
];
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
protected function setLayouts(array $layouts = [])
|
||||
{
|
||||
$this->layouts = array_map(function ($layout) {
|
||||
return Str::split($layout);
|
||||
}, $layouts);
|
||||
}
|
||||
|
||||
protected function setSettings(array $settings = [])
|
||||
{
|
||||
if (empty($settings) === true) {
|
||||
$this->settings = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$settings['icon'] = 'dashboard';
|
||||
$settings['type'] = 'layout';
|
||||
$settings['parent'] = $this->model();
|
||||
|
||||
$this->settings = Fieldset::factory($settings);
|
||||
}
|
||||
|
||||
public function settings()
|
||||
{
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
public function store($value)
|
||||
{
|
||||
$value = Layouts::factory($value, ['parent' => $this->model])->toArray();
|
||||
|
||||
foreach ($value as $layoutIndex => $layout) {
|
||||
if ($this->settings !== null) {
|
||||
$value[$layoutIndex]['attrs'] = $this->attrsForm($layout['attrs'])->content();
|
||||
}
|
||||
|
||||
foreach ($layout['columns'] as $columnIndex => $column) {
|
||||
$value[$layoutIndex]['columns'][$columnIndex]['blocks'] = $this->blocksToValues($column['blocks'] ?? [], 'content');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->valueToJson($value, $this->pretty());
|
||||
}
|
||||
|
||||
public function validations(): array
|
||||
{
|
||||
return [
|
||||
'layout' => function ($value) {
|
||||
$fields = [];
|
||||
$layoutIndex = 0;
|
||||
|
||||
foreach ($value as $layout) {
|
||||
$layoutIndex++;
|
||||
|
||||
// validate settings form
|
||||
foreach ($this->attrsForm($layout['attrs'] ?? [])->fields() as $field) {
|
||||
$errors = $field->errors();
|
||||
|
||||
if (empty($errors) === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'layout.validation.settings',
|
||||
'data' => [
|
||||
'index' => $layoutIndex
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// validate blocks in the layout
|
||||
$blockIndex = 0;
|
||||
|
||||
foreach ($layout['columns'] ?? [] as $column) {
|
||||
foreach ($column['blocks'] ?? [] as $block) {
|
||||
$blockIndex++;
|
||||
$blockType = $block['type'];
|
||||
|
||||
try {
|
||||
$blockFields = $fields[$blockType] ?? $this->fields($blockType) ?? [];
|
||||
} catch (Throwable $e) {
|
||||
// skip invalid blocks
|
||||
continue;
|
||||
}
|
||||
|
||||
// store the fields for the next round
|
||||
$fields[$blockType] = $blockFields;
|
||||
|
||||
// overwrite the content with the serialized form
|
||||
foreach ($this->form($blockFields, $block['content'])->fields() as $field) {
|
||||
$errors = $field->errors();
|
||||
|
||||
// rough first validation
|
||||
if (empty($errors) === false) {
|
||||
throw new InvalidArgumentException([
|
||||
'key' => 'layout.validation.block',
|
||||
'data' => [
|
||||
'blockIndex' => $blockIndex,
|
||||
'layoutIndex' => $layoutIndex
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
627
kirby/src/Form/FieldClass.php
Executable file
627
kirby/src/Form/FieldClass.php
Executable file
@@ -0,0 +1,627 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Form;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Cms\HasSiblings;
|
||||
use Kirby\Cms\ModelWithContent;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Throwable;
|
||||
|
||||
abstract class FieldClass
|
||||
{
|
||||
use HasSiblings;
|
||||
|
||||
protected $after;
|
||||
protected $autofocus;
|
||||
protected $before;
|
||||
protected $default;
|
||||
protected $disabled;
|
||||
protected $help;
|
||||
protected $icon;
|
||||
protected $label;
|
||||
/**
|
||||
* @var \Kirby\Cms\ModelWithContent
|
||||
*/
|
||||
protected $model;
|
||||
protected $name;
|
||||
protected $params;
|
||||
protected $placeholder;
|
||||
protected $required;
|
||||
protected $siblings;
|
||||
protected $translate;
|
||||
protected $value;
|
||||
protected $when;
|
||||
protected $width;
|
||||
|
||||
public function __call(string $param, array $args)
|
||||
{
|
||||
if (isset($this->$param) === true) {
|
||||
return $this->$param;
|
||||
}
|
||||
|
||||
return $this->params[$param] ?? null;
|
||||
}
|
||||
|
||||
public function __construct(array $params = [])
|
||||
{
|
||||
$this->params = $params;
|
||||
|
||||
$this->setAfter($params['after'] ?? null);
|
||||
$this->setAutofocus($params['autofocus'] ?? false);
|
||||
$this->setBefore($params['before'] ?? null);
|
||||
$this->setDefault($params['default'] ?? null);
|
||||
$this->setDisabled($params['disabled'] ?? false);
|
||||
$this->setHelp($params['help'] ?? null);
|
||||
$this->setIcon($params['icon'] ?? null);
|
||||
$this->setLabel($params['label'] ?? null);
|
||||
$this->setModel($params['model'] ?? site());
|
||||
$this->setName($params['name'] ?? null);
|
||||
$this->setPlaceholder($params['placeholder'] ?? null);
|
||||
$this->setRequired($params['required'] ?? false);
|
||||
$this->setSiblings($params['siblings'] ?? null);
|
||||
$this->setTranslate($params['translate'] ?? true);
|
||||
$this->setWhen($params['when'] ?? null);
|
||||
$this->setWidth($params['width'] ?? null);
|
||||
|
||||
if (array_key_exists('value', $params) === true) {
|
||||
$this->fill($params['value']);
|
||||
}
|
||||
}
|
||||
|
||||
public function after(): ?string
|
||||
{
|
||||
return $this->stringTemplate($this->after);
|
||||
}
|
||||
|
||||
public function api(): array
|
||||
{
|
||||
return $this->routes();
|
||||
}
|
||||
|
||||
public function autofocus(): bool
|
||||
{
|
||||
return $this->autofocus;
|
||||
}
|
||||
|
||||
public function before(): ?string
|
||||
{
|
||||
return $this->stringTemplate($this->before);
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED!
|
||||
*
|
||||
* Returns the field data
|
||||
* in a format to be stored
|
||||
* in Kirby's content fields
|
||||
*
|
||||
* @param bool $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function data(bool $default = false)
|
||||
{
|
||||
return $this->store($this->value($default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default value for the field,
|
||||
* which will be used when a page/file/user is created
|
||||
*/
|
||||
public function default()
|
||||
{
|
||||
if ($this->default === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_string($this->default) === false) {
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
return $this->stringTemplate($this->default);
|
||||
}
|
||||
|
||||
/**
|
||||
* If `true`, the field is no longer editable and will not be saved
|
||||
*/
|
||||
public function disabled(): bool
|
||||
{
|
||||
return $this->disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional help text below the field
|
||||
*/
|
||||
public function help(): ?string
|
||||
{
|
||||
if (empty($this->help) === false) {
|
||||
$help = $this->stringTemplate($this->help);
|
||||
$help = $this->kirby()->kirbytext($help);
|
||||
return $help;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function i18n($param)
|
||||
{
|
||||
return empty($param) === false ? I18n::translate($param, $param) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional icon that will be shown at the end of the field
|
||||
*/
|
||||
public function icon(): ?string
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->name();
|
||||
}
|
||||
|
||||
public function isDisabled(): bool
|
||||
{
|
||||
return $this->disabled;
|
||||
}
|
||||
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->isEmptyValue($this->value());
|
||||
}
|
||||
|
||||
public function isEmptyValue($value): bool
|
||||
{
|
||||
return in_array($value, [null, '', []], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field is invalid
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInvalid(): bool
|
||||
{
|
||||
return $this->isValid() === false;
|
||||
}
|
||||
|
||||
public function isRequired(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
public function isSaveable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field is valid
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
return empty($this->errors()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all validations and returns an array of
|
||||
* error messages
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function errors(): array
|
||||
{
|
||||
return $this->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the value
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
public function fill($value = null)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Kirby instance
|
||||
*
|
||||
* @return \Kirby\Cms\App
|
||||
*/
|
||||
public function kirby()
|
||||
{
|
||||
return $this->model->kirby();
|
||||
}
|
||||
|
||||
/**
|
||||
* The field label can be set as string or associative array with translations
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return $this->stringTemplate($this->label ?? Str::ucfirst($this->name()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent model
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function model()
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the field name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name ?? $this->type();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the field needs a value before being saved;
|
||||
* this is the case if all of the following requirements are met:
|
||||
* - The field is saveable
|
||||
* - The field is required
|
||||
* - The field is currently empty
|
||||
* - The field is not currently inactive because of a `when` rule
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function needsValue(): bool
|
||||
{
|
||||
// check simple conditions first
|
||||
if (
|
||||
$this->isSaveable() === false ||
|
||||
$this->isRequired() === false ||
|
||||
$this->isEmpty() === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check the data of the relevant fields if there is a `when` option
|
||||
if (empty($this->when) === false && is_array($this->when) === true) {
|
||||
$formFields = $this->siblings();
|
||||
|
||||
if ($formFields !== null) {
|
||||
foreach ($this->when as $field => $value) {
|
||||
$field = $formFields->get($field);
|
||||
$inputValue = $field !== null ? $field->value() : '';
|
||||
|
||||
// if the input data doesn't match the requested `when` value,
|
||||
// that means that this field is not required and can be saved
|
||||
// (*all* `when` conditions must be met for this field to be required)
|
||||
if ($inputValue !== $value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// either there was no `when` condition or all conditions matched
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all original params for the field
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function params(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional placeholder value that will be shown when the field is empty
|
||||
*/
|
||||
public function placeholder(): ?string
|
||||
{
|
||||
return $this->stringTemplate($this->placeholder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the props that will be sent to
|
||||
* the Vue component
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function props(): array
|
||||
{
|
||||
return [
|
||||
'after' => $this->after(),
|
||||
'autofocus' => $this->autofocus(),
|
||||
'before' => $this->before(),
|
||||
'default' => $this->default(),
|
||||
'disabled' => $this->isDisabled(),
|
||||
'help' => $this->help(),
|
||||
'icon' => $this->icon(),
|
||||
'label' => $this->label(),
|
||||
'name' => $this->name(),
|
||||
'placeholder' => $this->placeholder(),
|
||||
'required' => $this->isRequired(),
|
||||
'saveable' => $this->isSaveable(),
|
||||
'translate' => $this->translate(),
|
||||
'type' => $this->type(),
|
||||
'width' => $this->width(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* If `true`, the field has to be filled in correctly to be saved.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function required(): bool
|
||||
{
|
||||
return $this->required;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes for the field API
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function routes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* DEPRECATED
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
return $this->isSaveable();
|
||||
}
|
||||
|
||||
protected function setAfter($after = null)
|
||||
{
|
||||
$this->after = $this->i18n($after);
|
||||
}
|
||||
|
||||
protected function setAutofocus(bool $autofocus = false)
|
||||
{
|
||||
$this->autofocus = $autofocus;
|
||||
}
|
||||
|
||||
protected function setBefore($before = null)
|
||||
{
|
||||
$this->before = $this->i18n($before);
|
||||
}
|
||||
|
||||
protected function setDefault($default = null)
|
||||
{
|
||||
$this->default = $default;
|
||||
}
|
||||
|
||||
protected function setDisabled(bool $disabled = false)
|
||||
{
|
||||
$this->disabled = $disabled;
|
||||
}
|
||||
|
||||
protected function setHelp($help = null)
|
||||
{
|
||||
$this->help = $this->i18n($help);
|
||||
}
|
||||
|
||||
protected function setIcon(string $icon = null)
|
||||
{
|
||||
$this->icon = $icon;
|
||||
}
|
||||
|
||||
protected function setLabel($label = null)
|
||||
{
|
||||
$this->label = $this->i18n($label);
|
||||
}
|
||||
|
||||
protected function setModel(ModelWithContent $model)
|
||||
{
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
protected function setName(string $name = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
protected function setPlaceholder($placeholder = null)
|
||||
{
|
||||
$this->placeholder = $this->i18n($placeholder);
|
||||
}
|
||||
|
||||
protected function setRequired(bool $required = false)
|
||||
{
|
||||
$this->required = $required;
|
||||
}
|
||||
|
||||
protected function setSiblings(Fields $siblings = null)
|
||||
{
|
||||
$this->siblings = $siblings ?? new Fields([]);
|
||||
}
|
||||
|
||||
protected function setTranslate(bool $translate = true)
|
||||
{
|
||||
$this->translate = $translate;
|
||||
}
|
||||
|
||||
protected function setWhen($when = null)
|
||||
{
|
||||
$this->when = $when;
|
||||
}
|
||||
|
||||
protected function setWidth(string $width = null)
|
||||
{
|
||||
$this->width = $width;
|
||||
}
|
||||
|
||||
protected function siblingsCollection()
|
||||
{
|
||||
return $this->siblings;
|
||||
}
|
||||
|
||||
protected function stringTemplate(?string $string = null): ?string
|
||||
{
|
||||
if ($string !== null) {
|
||||
return $this->model->toString($string);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function store($value)
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should the field be translatable?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function translate(): bool
|
||||
{
|
||||
return $this->translate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the field to a plain array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$props = $this->props();
|
||||
$props['signature'] = md5(json_encode($props));
|
||||
|
||||
ksort($props);
|
||||
|
||||
return array_filter($props, function ($item) {
|
||||
return $item !== null;
|
||||
});
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return lcfirst(basename(str_replace(['\\', 'Field'], ['/', ''], static::class)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the validations defined for the field
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function validate(): array
|
||||
{
|
||||
$validations = $this->validations();
|
||||
$value = $this->value();
|
||||
$errors = [];
|
||||
|
||||
// validate required values
|
||||
if ($this->needsValue() === true) {
|
||||
$errors['required'] = I18n::translate('error.validation.required');
|
||||
}
|
||||
|
||||
foreach ($validations as $key => $validation) {
|
||||
if (is_int($key) === true) {
|
||||
// predefined validation
|
||||
try {
|
||||
Validations::$validation($this, $value);
|
||||
} catch (Exception $e) {
|
||||
$errors[$validation] = $e->getMessage();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_a($validation, 'Closure') === true) {
|
||||
try {
|
||||
$validation->call($this, $value);
|
||||
} catch (Exception $e) {
|
||||
$errors[$key] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines all validation rules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function validations(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the field if saveable
|
||||
* otherwise it returns null
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function value(bool $default = false)
|
||||
{
|
||||
if ($this->isSaveable() === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($default === true && $this->isEmpty() === true) {
|
||||
return $this->default();
|
||||
}
|
||||
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
protected function valueFromJson($value): array
|
||||
{
|
||||
try {
|
||||
return Data::decode($value, 'json');
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected function valueFromYaml($value)
|
||||
{
|
||||
return Data::decode($value, 'yaml');
|
||||
}
|
||||
|
||||
protected function valueToJson(array $value = null, bool $pretty = false): string
|
||||
{
|
||||
if ($pretty === true) {
|
||||
return json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
return json_encode($value);
|
||||
}
|
||||
|
||||
protected function valueToYaml(array $value = null): string
|
||||
{
|
||||
return Data::encode($value, 'yaml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the width of the field in
|
||||
* the Panel grid
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function width(): string
|
||||
{
|
||||
return $this->width ?? '1/1';
|
||||
}
|
||||
}
|
@@ -30,7 +30,7 @@ class Fields extends Collection
|
||||
if (is_array($field) === true) {
|
||||
// use the array key as name if the name is not set
|
||||
$field['name'] = $field['name'] ?? $name;
|
||||
$field = new Field($field['type'], $field);
|
||||
$field = Field::factory($field['type'], $field, $this);
|
||||
}
|
||||
|
||||
return parent::__set($field->name(), $field);
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Kirby\Form;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Data\Data;
|
||||
use Throwable;
|
||||
|
||||
@@ -81,7 +82,7 @@ class Form
|
||||
}
|
||||
|
||||
try {
|
||||
$field = new Field($props['type'], $props, $this->fields);
|
||||
$field = Field::factory($props['type'], $props, $this->fields);
|
||||
} catch (Throwable $e) {
|
||||
$field = static::exceptionField($e, $props);
|
||||
}
|
||||
@@ -178,13 +179,19 @@ class Form
|
||||
*/
|
||||
public static function exceptionField(Throwable $exception, array $props = [])
|
||||
{
|
||||
$message = $exception->getMessage();
|
||||
|
||||
if (App::instance()->option('debug') === true) {
|
||||
$message .= ' in file: ' . $exception->getFile() . ' line: ' . $exception->getLine();
|
||||
}
|
||||
|
||||
$props = array_merge($props, [
|
||||
'label' => 'Error in "' . $props['name'] . '" field',
|
||||
'label' => 'Error in "' . $props['name'] . '" field.',
|
||||
'theme' => 'negative',
|
||||
'text' => strip_tags($exception->getMessage()),
|
||||
'text' => strip_tags($message),
|
||||
]);
|
||||
|
||||
return new Field('info', $props);
|
||||
return Field::factory('info', $props);
|
||||
}
|
||||
|
||||
/**
|
||||
|
18
kirby/src/Form/Mixin/EmptyState.php
Executable file
18
kirby/src/Form/Mixin/EmptyState.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Form\Mixin;
|
||||
|
||||
trait EmptyState
|
||||
{
|
||||
protected $empty;
|
||||
|
||||
protected function setEmpty($empty = null)
|
||||
{
|
||||
$this->empty = $this->i18n($empty);
|
||||
}
|
||||
|
||||
public function empty(): ?string
|
||||
{
|
||||
return $this->stringTemplate($this->empty);
|
||||
}
|
||||
}
|
18
kirby/src/Form/Mixin/Max.php
Executable file
18
kirby/src/Form/Mixin/Max.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Form\Mixin;
|
||||
|
||||
trait Max
|
||||
{
|
||||
protected $max;
|
||||
|
||||
public function max(): ?int
|
||||
{
|
||||
return $this->max;
|
||||
}
|
||||
|
||||
protected function setMax(int $max = null)
|
||||
{
|
||||
$this->max = $max;
|
||||
}
|
||||
}
|
18
kirby/src/Form/Mixin/Min.php
Executable file
18
kirby/src/Form/Mixin/Min.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Form\Mixin;
|
||||
|
||||
trait Min
|
||||
{
|
||||
protected $min;
|
||||
|
||||
public function min(): ?int
|
||||
{
|
||||
return $this->min;
|
||||
}
|
||||
|
||||
protected function setMin(int $min = null)
|
||||
{
|
||||
$this->min = $min;
|
||||
}
|
||||
}
|
@@ -56,14 +56,8 @@ class Cookie
|
||||
$_COOKIE[$key] = $value;
|
||||
|
||||
// store the cookie
|
||||
// the array syntax is only supported by PHP 7.3+
|
||||
// TODO: Always use the first alternative when support for PHP 7.2 is dropped
|
||||
if (version_compare(PHP_VERSION, '7.3.0', '>=') === true) {
|
||||
$options = compact('expires', 'path', 'domain', 'secure', 'httponly', 'samesite');
|
||||
return setcookie($key, $value, $options);
|
||||
} else {
|
||||
return setcookie($key, $value, $expires, $path, $domain, $secure, $httponly);
|
||||
}
|
||||
$options = compact('expires', 'path', 'domain', 'secure', 'httponly', 'samesite');
|
||||
return setcookie($key, $value, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -93,7 +93,7 @@ class Route
|
||||
* @param Closure $action
|
||||
* @param array $attributes
|
||||
*/
|
||||
public function __construct($pattern, $method = 'GET', Closure $action, array $attributes = [])
|
||||
public function __construct($pattern, $method, Closure $action, array $attributes = [])
|
||||
{
|
||||
$this->action = $action;
|
||||
$this->attributes = $attributes;
|
||||
|
@@ -114,7 +114,7 @@ class Dimensions
|
||||
*/
|
||||
public function fit(int $box, bool $force = false)
|
||||
{
|
||||
if ($this->width == 0 || $this->height == 0) {
|
||||
if ($this->width === 0 || $this->height === 0) {
|
||||
$this->width = $box;
|
||||
$this->height = $box;
|
||||
return $this;
|
||||
@@ -379,7 +379,7 @@ class Dimensions
|
||||
*/
|
||||
public function square(): bool
|
||||
{
|
||||
return $this->width == $this->height;
|
||||
return $this->width === $this->height;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -2,11 +2,10 @@
|
||||
|
||||
namespace Kirby\Image;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\Exception;
|
||||
use Kirby\Http\Response;
|
||||
use Kirby\Toolkit\File;
|
||||
use Kirby\Toolkit\Html;
|
||||
use Kirby\Toolkit\I18n;
|
||||
use Kirby\Toolkit\Mime;
|
||||
use Kirby\Toolkit\V;
|
||||
|
||||
@@ -204,15 +203,44 @@ class Image extends File
|
||||
*/
|
||||
public function match(array $rules): bool
|
||||
{
|
||||
if (($rules['mime'] ?? null) !== null) {
|
||||
if (Mime::isAccepted($this->mime(), $rules['mime']) !== true) {
|
||||
throw new Exception(I18n::template('error.file.mime.invalid', [
|
||||
'mime' => $this->mime()
|
||||
]));
|
||||
$rules = array_change_key_case($rules);
|
||||
|
||||
if (is_array($rules['mime'] ?? null) === true) {
|
||||
$mime = $this->mime();
|
||||
|
||||
// determine if any pattern matches the MIME type;
|
||||
// once any pattern matches, `$carry` is `true` and the rest is skipped
|
||||
$matches = array_reduce($rules['mime'], function ($carry, $pattern) use ($mime) {
|
||||
return $carry || Mime::matches($mime, $pattern);
|
||||
}, false);
|
||||
|
||||
if ($matches !== true) {
|
||||
throw new Exception([
|
||||
'key' => 'file.mime.invalid',
|
||||
'data' => compact('mime')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$rules = array_change_key_case($rules);
|
||||
if (is_array($rules['extension'] ?? null) === true) {
|
||||
$extension = $this->extension();
|
||||
if (in_array($extension, $rules['extension']) !== true) {
|
||||
throw new Exception([
|
||||
'key' => 'file.extension.invalid',
|
||||
'data' => compact('extension')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($rules['type'] ?? null) === true) {
|
||||
$type = $this->type();
|
||||
if (in_array($type, $rules['type']) !== true) {
|
||||
throw new Exception([
|
||||
'key' => 'file.type.invalid',
|
||||
'data' => compact('type')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$validations = [
|
||||
'maxsize' => ['size', 'max'],
|
||||
@@ -232,9 +260,10 @@ class Image extends File
|
||||
$validator = $arguments[1];
|
||||
|
||||
if (V::$validator($this->$property(), $rule) === false) {
|
||||
throw new Exception(I18n::template('error.file.' . $key, [
|
||||
$property => $rule
|
||||
]));
|
||||
throw new Exception([
|
||||
'key' => 'file.' . $key,
|
||||
'data' => [$property => $rule]
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -79,7 +79,7 @@ class Location
|
||||
$seconds = count($coord) > 2 ? $this->num($coord[2]) : 0;
|
||||
|
||||
$hemi = strtoupper($hemi);
|
||||
$flip = ($hemi == 'W' || $hemi == 'S') ? -1 : 1;
|
||||
$flip = ($hemi === 'W' || $hemi === 'S') ? -1 : 1;
|
||||
|
||||
return $flip * ($degrees + $minutes / 60 + $seconds / 3600);
|
||||
}
|
||||
@@ -94,7 +94,7 @@ class Location
|
||||
{
|
||||
$parts = explode('/', $part);
|
||||
|
||||
if (count($parts) == 1) {
|
||||
if (count($parts) === 1) {
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
|
100
kirby/src/Parsley/Element.php
Executable file
100
kirby/src/Parsley/Element.php
Executable file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Parsley;
|
||||
|
||||
use DOMElement;
|
||||
use DOMXpath;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
class Element
|
||||
{
|
||||
protected $marks;
|
||||
protected $node;
|
||||
|
||||
public function __construct(DOMElement $node, array $marks = [])
|
||||
{
|
||||
$this->marks = $marks;
|
||||
$this->node = $node;
|
||||
}
|
||||
|
||||
public function attr(string $attr, $fallback = null)
|
||||
{
|
||||
if ($this->node->hasAttribute($attr)) {
|
||||
return $this->node->getAttribute($attr) ?? $fallback;
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->node->childNodes;
|
||||
}
|
||||
|
||||
public function classList(): array
|
||||
{
|
||||
return Str::split($this->className(), ' ');
|
||||
}
|
||||
|
||||
public function className()
|
||||
{
|
||||
return $this->node->getAttribute('class');
|
||||
}
|
||||
|
||||
public function element()
|
||||
{
|
||||
return $this->node;
|
||||
}
|
||||
|
||||
public function filter(string $query)
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if ($queryResult = $this->query($query)) {
|
||||
foreach ($queryResult as $node) {
|
||||
$result[] = new static($node);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function find(string $query)
|
||||
{
|
||||
if ($result = $this->query($query)[0]) {
|
||||
return new static($result);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function innerHtml(array $marks = null): string
|
||||
{
|
||||
return (new Inline($this->node, $marks ?? $this->marks))->innerHtml();
|
||||
}
|
||||
|
||||
public function innerText()
|
||||
{
|
||||
return trim($this->node->textContent);
|
||||
}
|
||||
|
||||
public function outerHtml(array $marks = null): string
|
||||
{
|
||||
return $this->node->ownerDocument->saveHtml($this->node);
|
||||
}
|
||||
|
||||
public function query($query)
|
||||
{
|
||||
return (new DOMXPath($this->node->ownerDocument))->query($query, $this->node);
|
||||
}
|
||||
|
||||
public function remove()
|
||||
{
|
||||
$this->node->parentNode->removeChild($this->node);
|
||||
}
|
||||
|
||||
public function tagName(): string
|
||||
{
|
||||
return $this->node->tagName;
|
||||
}
|
||||
}
|
74
kirby/src/Parsley/Inline.php
Executable file
74
kirby/src/Parsley/Inline.php
Executable file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Parsley;
|
||||
|
||||
class Inline
|
||||
{
|
||||
protected $html = '';
|
||||
protected $marks = [];
|
||||
|
||||
public function __construct($node, array $marks = [])
|
||||
{
|
||||
$this->createMarkRules($marks);
|
||||
$this->html = trim($this->parseNode($node));
|
||||
}
|
||||
|
||||
public function createMarkRules($marks)
|
||||
{
|
||||
foreach ($marks as $mark) {
|
||||
$this->marks[$mark['tag']] = $mark;
|
||||
}
|
||||
}
|
||||
|
||||
public function parseChildren($children): string
|
||||
{
|
||||
if (!$children) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = '';
|
||||
foreach ($children as $child) {
|
||||
$html .= $this->parseNode($child);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function parseNode($node)
|
||||
{
|
||||
$html = '';
|
||||
|
||||
if (is_a($node, 'DOMText') === true) {
|
||||
return $node->textContent;
|
||||
}
|
||||
|
||||
// ignore comments
|
||||
if (is_a($node, 'DOMComment') === true) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// known marks
|
||||
if (array_key_exists($node->tagName, $this->marks) === true) {
|
||||
$mark = $this->marks[$node->tagName];
|
||||
$attrs = [];
|
||||
$defaults = $mark['defaults'] ?? [];
|
||||
|
||||
foreach ($mark['attrs'] ?? [] as $attr) {
|
||||
if ($node->hasAttribute($attr)) {
|
||||
$attrs[$attr] = $node->getAttribute($attr);
|
||||
} else {
|
||||
$attrs[$attr] = $defaults[$attr] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return '<' . $node->tagName . attr($attrs, ' ') . '>' . $this->parseChildren($node->childNodes) . '</' . $node->tagName . '>';
|
||||
}
|
||||
|
||||
// unknown marks
|
||||
return $this->parseChildren($node->childNodes);
|
||||
}
|
||||
|
||||
public function innerHtml()
|
||||
{
|
||||
return $this->html;
|
||||
}
|
||||
}
|
231
kirby/src/Parsley/Parsley.php
Executable file
231
kirby/src/Parsley/Parsley.php
Executable file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Parsley;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
use Kirby\Parsley\Schema\Plain;
|
||||
|
||||
class Parsley
|
||||
{
|
||||
protected $blocks = [];
|
||||
protected $body;
|
||||
protected $doc;
|
||||
protected $marks = [];
|
||||
protected $nodes = [];
|
||||
protected $schema;
|
||||
protected $skip = [];
|
||||
|
||||
public static $useXmlExtension = true;
|
||||
|
||||
public function __construct(string $html, Schema $schema = null)
|
||||
{
|
||||
// fail gracefully if the XML extension is not installed
|
||||
// or should be skipped
|
||||
if ($this->useXmlExtension() === false) {
|
||||
$this->blocks[] = [
|
||||
'type' => 'markdown',
|
||||
'content' => [
|
||||
'text' => $html,
|
||||
]
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
|
||||
$this->doc = new DOMDocument();
|
||||
$this->doc->preserveWhiteSpace = false;
|
||||
$this->doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
libxml_clear_errors();
|
||||
|
||||
$this->schema = $schema ?? new Plain();
|
||||
$this->skip = $this->schema->skip();
|
||||
$this->marks = $this->schema->marks();
|
||||
$this->inline = [];
|
||||
|
||||
$this->createNodeRules($this->schema->nodes());
|
||||
|
||||
$this->parseNode($this->body());
|
||||
$this->endInlineBlock();
|
||||
}
|
||||
|
||||
public function blocks(): array
|
||||
{
|
||||
return $this->blocks;
|
||||
}
|
||||
|
||||
public function body()
|
||||
{
|
||||
return $this->body = $this->body ?? $this->query($this->doc, '/html/body')[0];
|
||||
}
|
||||
|
||||
public function createNodeRules($nodes)
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
$this->nodes[$node['tag']] = $node;
|
||||
}
|
||||
}
|
||||
|
||||
public function containsBlock($element): bool
|
||||
{
|
||||
if (!$element->childNodes) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($element->childNodes as $childNode) {
|
||||
if ($this->isBlock($childNode) === true || $this->containsBlock($childNode)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function endInlineBlock()
|
||||
{
|
||||
$html = [];
|
||||
|
||||
foreach ($this->inline as $inline) {
|
||||
$node = new Inline($inline, $this->marks);
|
||||
$html[] = $node->innerHTML();
|
||||
}
|
||||
|
||||
$innerHTML = implode(' ', $html);
|
||||
|
||||
if ($fallback = $this->fallback($innerHTML)) {
|
||||
$this->mergeOrAppend($fallback);
|
||||
}
|
||||
|
||||
$this->inline = [];
|
||||
}
|
||||
|
||||
public function fallback($node)
|
||||
{
|
||||
if (is_a($node, 'DOMText') === true) {
|
||||
$html = $node->textContent;
|
||||
} elseif (is_a($node, Element::class) === true) {
|
||||
$html = $node->innerHtml();
|
||||
} elseif (is_string($node) === true) {
|
||||
$html = $node;
|
||||
} else {
|
||||
$html = '';
|
||||
}
|
||||
|
||||
if ($fallback = $this->schema->fallback($html)) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isBlock($element): bool
|
||||
{
|
||||
if (is_a($element, 'DOMElement') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_key_exists($element->tagName, $this->nodes) === true;
|
||||
}
|
||||
|
||||
public function isInline($element)
|
||||
{
|
||||
if (is_a($element, 'DOMText') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_a($element, 'DOMElement') === true) {
|
||||
if ($this->containsBlock($element) === true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($element->tagName === 'p') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$marks = array_column($this->marks, 'tag');
|
||||
return in_array($element->tagName, $marks);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function mergeOrAppend($block)
|
||||
{
|
||||
$lastIndex = count($this->blocks) - 1;
|
||||
$lastItem = $this->blocks[$lastIndex] ?? null;
|
||||
|
||||
// merge with previous block
|
||||
if ($block['type'] === 'text' && $lastItem && $lastItem['type'] === 'text') {
|
||||
$this->blocks[$lastIndex]['content']['text'] .= "\n\n" . $block['content']['text'];
|
||||
|
||||
// append
|
||||
} else {
|
||||
$this->blocks[] = $block;
|
||||
}
|
||||
}
|
||||
|
||||
public function parseNode($element)
|
||||
{
|
||||
// comments
|
||||
if (is_a($element, 'DOMComment') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// inline context
|
||||
if ($this->isInline($element)) {
|
||||
$this->inline[] = $element;
|
||||
return true;
|
||||
} else {
|
||||
$this->endInlineBlock();
|
||||
}
|
||||
|
||||
// known block nodes
|
||||
if ($this->isBlock($element) === true) {
|
||||
if ($parser = ($this->nodes[$element->tagName]['parse'] ?? null)) {
|
||||
if ($result = $parser(new Element($element, $this->marks))) {
|
||||
$this->blocks[] = $result;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// has only unkown children (div, etc.)
|
||||
if ($this->containsBlock($element) === false) {
|
||||
if (in_array($element->tagName, $this->skip) === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($element->tagName !== 'body') {
|
||||
$node = new Element($element, $this->marks);
|
||||
|
||||
if ($block = $this->fallback($node)) {
|
||||
$this->mergeOrAppend($block);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// parse all children
|
||||
foreach ($element->childNodes as $childNode) {
|
||||
$this->parseNode($childNode);
|
||||
}
|
||||
}
|
||||
|
||||
public function query($element, $query)
|
||||
{
|
||||
return (new DOMXPath($element))->query($query);
|
||||
}
|
||||
|
||||
public function useXmlExtension(): bool
|
||||
{
|
||||
if (static::$useXmlExtension !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return class_exists('DOMDocument') === true;
|
||||
}
|
||||
}
|
11
kirby/src/Parsley/Schema.php
Executable file
11
kirby/src/Parsley/Schema.php
Executable file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Parsley;
|
||||
|
||||
abstract class Schema
|
||||
{
|
||||
abstract public function fallback(string $html);
|
||||
abstract public function marks(): array;
|
||||
abstract public function nodes(): array;
|
||||
abstract public function skip(): array;
|
||||
}
|
317
kirby/src/Parsley/Schema/Blocks.php
Executable file
317
kirby/src/Parsley/Schema/Blocks.php
Executable file
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Parsley\Schema;
|
||||
|
||||
use Kirby\Parsley\Element;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
class Blocks extends Plain
|
||||
{
|
||||
public function fallback(string $html)
|
||||
{
|
||||
$html = trim($html);
|
||||
|
||||
if (Str::length($html) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => [
|
||||
'text' => '<p>' . $html . '</p>',
|
||||
],
|
||||
'type' => 'text',
|
||||
];
|
||||
}
|
||||
|
||||
public function heading($node, $level)
|
||||
{
|
||||
$content = [
|
||||
'level' => $level,
|
||||
'text' => $node->innerHTML()
|
||||
];
|
||||
|
||||
if ($id = $node->attr('id')) {
|
||||
$content['id'] = $id;
|
||||
}
|
||||
|
||||
ksort($content);
|
||||
|
||||
return [
|
||||
'content' => $content,
|
||||
'type' => 'heading',
|
||||
];
|
||||
}
|
||||
|
||||
public function list($node)
|
||||
{
|
||||
$html = [];
|
||||
|
||||
foreach ($node->filter('li') as $li) {
|
||||
$innerHtml = '';
|
||||
|
||||
foreach ($li->children() as $child) {
|
||||
if (is_a($child, 'DOMText') === true) {
|
||||
$innerHtml .= $child->textContent;
|
||||
} elseif (is_a($child, 'DOMElement') === true) {
|
||||
$child = new Element($child);
|
||||
|
||||
if (in_array($child->tagName(), ['ul', 'ol']) === true) {
|
||||
$innerHtml .= $this->list($child);
|
||||
} else {
|
||||
$innerHtml .= $child->innerHTML($this->marks());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$html[] = '<li>' . trim($innerHtml) . '</li>';
|
||||
}
|
||||
|
||||
return '<' . $node->tagName() . '>' . implode($html) . '</' . $node->tagName() . '>';
|
||||
}
|
||||
|
||||
public function marks(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'tag' => 'a',
|
||||
'attrs' => ['href', 'target', 'title'],
|
||||
],
|
||||
[
|
||||
'tag' => 'abbr',
|
||||
],
|
||||
[
|
||||
'tag' => 'b'
|
||||
],
|
||||
[
|
||||
'tag' => 'code'
|
||||
],
|
||||
[
|
||||
'tag' => 'del',
|
||||
],
|
||||
[
|
||||
'tag' => 'em',
|
||||
],
|
||||
[
|
||||
'tag' => 'i',
|
||||
],
|
||||
[
|
||||
'tag' => 'strike',
|
||||
],
|
||||
[
|
||||
'tag' => 'sub',
|
||||
],
|
||||
[
|
||||
'tag' => 'sup',
|
||||
],
|
||||
[
|
||||
'tag' => 'strong',
|
||||
],
|
||||
[
|
||||
'tag' => 'u',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function nodes(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'tag' => 'blockquote',
|
||||
'parse' => function ($node) {
|
||||
$citation = null;
|
||||
$text = [];
|
||||
|
||||
// get all the text for the quote
|
||||
foreach ($node->element()->childNodes as $child) {
|
||||
if (is_a($child, 'DOMText') === true) {
|
||||
$text[] = trim($child->textContent);
|
||||
}
|
||||
if (is_a($child, 'DOMElement') === true && $child->tagName !== 'footer') {
|
||||
$text[] = (new Element($child))->innerHTML($this->marks());
|
||||
}
|
||||
}
|
||||
|
||||
// filter empty blocks and separate text blocks with breaks
|
||||
$text = implode('<br></br>', array_filter($text));
|
||||
|
||||
// get the citation from the footer
|
||||
if ($footer = $node->find('footer')) {
|
||||
$citation = $footer->innerHTML($this->marks());
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => [
|
||||
'citation' => $citation,
|
||||
'text' => $text
|
||||
],
|
||||
'type' => 'quote',
|
||||
];
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'h1',
|
||||
'parse' => function ($node) {
|
||||
return $this->heading($node, 'h1');
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'h2',
|
||||
'parse' => function ($node) {
|
||||
return $this->heading($node, 'h2');
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'h3',
|
||||
'parse' => function ($node) {
|
||||
return $this->heading($node, 'h3');
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'h4',
|
||||
'parse' => function ($node) {
|
||||
return $this->heading($node, 'h4');
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'h5',
|
||||
'parse' => function ($node) {
|
||||
return $this->heading($node, 'h5');
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'h6',
|
||||
'parse' => function ($node) {
|
||||
return $this->heading($node, 'h6');
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'iframe',
|
||||
'parse' => function ($node) {
|
||||
$caption = null;
|
||||
$src = $node->attr('src');
|
||||
|
||||
if ($figcaption = $node->find('ancestor::figure[1]//figcaption')) {
|
||||
$caption = $figcaption->innerHTML($this->marks());
|
||||
|
||||
// avoid parsing the caption twice
|
||||
$figcaption->remove();
|
||||
}
|
||||
|
||||
// reverse engineer video URLs
|
||||
if (preg_match('!player.vimeo.com\/video\/([0-9]+)!i', $src, $array) === 1) {
|
||||
$src = 'https://vimeo.com/' . $array[1];
|
||||
} elseif (preg_match('!youtube.com\/embed\/([a-zA-Z0-9_-]+)!', $src, $array) === 1) {
|
||||
$src = 'https://youtube.com/watch?v=' . $array[1];
|
||||
} elseif (preg_match('!youtube-nocookie.com\/embed\/([a-zA-Z0-9_-]+)!', $src, $array) === 1) {
|
||||
$src = 'https://youtube.com/watch?v=' . $array[1];
|
||||
} else {
|
||||
$src = false;
|
||||
}
|
||||
|
||||
// correct video URL
|
||||
if ($src) {
|
||||
return [
|
||||
'content' => [
|
||||
'caption' => $caption,
|
||||
'url' => $src
|
||||
],
|
||||
'type' => 'video',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $node->outerHTML()
|
||||
],
|
||||
'type' => 'markdown',
|
||||
];
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'img',
|
||||
'parse' => function ($node) {
|
||||
$caption = null;
|
||||
$link = null;
|
||||
|
||||
if ($figcaption = $node->find('ancestor::figure[1]//figcaption')) {
|
||||
$caption = $figcaption->innerHTML($this->marks());
|
||||
|
||||
// avoid parsing the caption twice
|
||||
$figcaption->remove();
|
||||
}
|
||||
|
||||
if ($a = $node->find('ancestor::a')) {
|
||||
$link = $a->attr('href');
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => [
|
||||
'alt' => $node->attr('alt'),
|
||||
'caption' => $caption,
|
||||
'link' => $link,
|
||||
'location' => 'web',
|
||||
'src' => $node->attr('src'),
|
||||
],
|
||||
'type' => 'image',
|
||||
];
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'ol',
|
||||
'parse' => function ($node) {
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $this->list($node)
|
||||
],
|
||||
'type' => 'list',
|
||||
];
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'pre',
|
||||
'parse' => function ($node) {
|
||||
$language = 'text';
|
||||
|
||||
if ($code = $node->find('//code')) {
|
||||
foreach ($code->classList() as $className) {
|
||||
if (preg_match('!language-(.*?)!', $className)) {
|
||||
$language = str_replace('language-', '', $className);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'content' => [
|
||||
'code' => $node->innerText(),
|
||||
'language' => $language
|
||||
],
|
||||
'type' => 'code',
|
||||
];
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'table',
|
||||
'parse' => function ($node) {
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $node->outerHTML(),
|
||||
],
|
||||
'type' => 'markdown',
|
||||
];
|
||||
}
|
||||
],
|
||||
[
|
||||
'tag' => 'ul',
|
||||
'parse' => function ($node) {
|
||||
return [
|
||||
'content' => [
|
||||
'text' => $this->list($node)
|
||||
],
|
||||
'type' => 'list',
|
||||
];
|
||||
}
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
40
kirby/src/Parsley/Schema/Plain.php
Executable file
40
kirby/src/Parsley/Schema/Plain.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Parsley\Schema;
|
||||
|
||||
use Kirby\Parsley\Schema;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
class Plain extends Schema
|
||||
{
|
||||
public function fallback(string $html)
|
||||
{
|
||||
$text = trim($html);
|
||||
|
||||
if (Str::length($text) === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'text',
|
||||
'content' => [
|
||||
'text' => $text
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function marks(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function nodes(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function skip(): array
|
||||
{
|
||||
return ['meta', 'script', 'style'];
|
||||
}
|
||||
}
|
@@ -141,13 +141,13 @@ class A
|
||||
*
|
||||
* @param array $array1
|
||||
* @param array $array2
|
||||
* @param bool $mode Behavior for elements with numeric keys;
|
||||
* A::MERGE_APPEND: elements are appended, keys are reset;
|
||||
* A::MERGE_OVERWRITE: elements are overwritten, keys are preserved
|
||||
* A::MERGE_REPLACE: non-associative arrays are completely replaced
|
||||
* @param int $mode Behavior for elements with numeric keys;
|
||||
* A::MERGE_APPEND: elements are appended, keys are reset;
|
||||
* A::MERGE_OVERWRITE: elements are overwritten, keys are preserved
|
||||
* A::MERGE_REPLACE: non-associative arrays are completely replaced
|
||||
* @return array
|
||||
*/
|
||||
public static function merge($array1, $array2, $mode = A::MERGE_APPEND)
|
||||
public static function merge($array1, $array2, int $mode = A::MERGE_APPEND)
|
||||
{
|
||||
$merged = $array1;
|
||||
|
||||
@@ -158,7 +158,7 @@ class A
|
||||
foreach ($array2 as $key => $value) {
|
||||
|
||||
// append to the merged array, don't overwrite numeric keys
|
||||
if (is_int($key) === true && $mode == static::MERGE_APPEND) {
|
||||
if (is_int($key) === true && $mode === static::MERGE_APPEND) {
|
||||
$merged[] = $value;
|
||||
|
||||
// recursively merge the two array values
|
||||
@@ -171,7 +171,7 @@ class A
|
||||
}
|
||||
}
|
||||
|
||||
if ($mode == static::MERGE_APPEND) {
|
||||
if ($mode === static::MERGE_APPEND) {
|
||||
// the keys don't make sense anymore, reset them
|
||||
// array_merge() is the simplest way to renumber
|
||||
// arrays that have both numeric and string keys;
|
||||
@@ -553,7 +553,7 @@ class A
|
||||
*/
|
||||
public static function sort(array $array, string $field, string $direction = 'desc', $method = SORT_REGULAR): array
|
||||
{
|
||||
$direction = strtolower($direction) == 'desc' ? SORT_DESC : SORT_ASC;
|
||||
$direction = strtolower($direction) === 'desc' ? SORT_DESC : SORT_ASC;
|
||||
$helper = [];
|
||||
$result = [];
|
||||
|
||||
|
@@ -236,55 +236,53 @@ class Collection extends Iterator implements Countable
|
||||
return $collection->set($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters elements by a custom
|
||||
* filter function or an array of filters
|
||||
*
|
||||
* @param array|\Closure $filter
|
||||
* @return self
|
||||
* @throws \Exception if $filter is neither a closure nor an array
|
||||
*/
|
||||
public function filter($filter)
|
||||
{
|
||||
if (is_callable($filter) === true) {
|
||||
$collection = clone $this;
|
||||
$collection->data = array_filter($this->data, $filter);
|
||||
|
||||
return $collection;
|
||||
} elseif (is_array($filter) === true) {
|
||||
$collection = $this;
|
||||
|
||||
foreach ($filter as $arguments) {
|
||||
$collection = $collection->filterBy(...$arguments);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
throw new Exception('The filter method needs either an array of filterBy rules or a closure function to be passed as parameter.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters elements by one of the
|
||||
* predefined filter methods.
|
||||
* predefined filter methods, by a
|
||||
* custom filter function or an array of filters
|
||||
*
|
||||
* @param string $field
|
||||
* @param string|array|\Closure $field
|
||||
* @param array ...$args
|
||||
* @return \Kirby\Toolkit\Collection
|
||||
*/
|
||||
public function filterBy(string $field, ...$args)
|
||||
public function filter($field, ...$args)
|
||||
{
|
||||
$operator = '==';
|
||||
$test = $args[0] ?? null;
|
||||
$split = $args[1] ?? false;
|
||||
|
||||
if (is_string($test) === true && isset(static::$filters[$test]) === true) {
|
||||
// filter by custom filter function
|
||||
if (is_callable($field) === true) {
|
||||
$collection = clone $this;
|
||||
$collection->data = array_filter($this->data, $field);
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
// array of filters
|
||||
if (is_array($field) === true) {
|
||||
$collection = $this;
|
||||
|
||||
foreach ($field as $filter) {
|
||||
$collection = $collection->filter(...$filter);
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
if (
|
||||
is_string($test) === true &&
|
||||
isset(static::$filters[$test]) === true
|
||||
) {
|
||||
$operator = $test;
|
||||
$test = $args[1] ?? null;
|
||||
$split = $args[2] ?? false;
|
||||
}
|
||||
|
||||
if (is_object($test) === true && method_exists($test, '__toString') === true) {
|
||||
if (
|
||||
is_object($test) === true &&
|
||||
method_exists($test, '__toString') === true
|
||||
) {
|
||||
$test = (string)$test;
|
||||
}
|
||||
|
||||
@@ -321,11 +319,18 @@ class Collection extends Iterator implements Countable
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $validator
|
||||
* @param mixed $values
|
||||
* @param mixed $test
|
||||
* @return bool
|
||||
* Alias for `Kirby\Toolkit\Collection::filter`
|
||||
*
|
||||
* @param string|Closure $field
|
||||
* @param array ...$args
|
||||
* @return self
|
||||
*/
|
||||
public function filterBy(...$args)
|
||||
{
|
||||
return $this->filter(...$args);
|
||||
}
|
||||
|
||||
|
||||
protected function filterMatchesAny($validator, $values, $test): bool
|
||||
{
|
||||
foreach ($values as $value) {
|
||||
@@ -472,7 +477,7 @@ class Collection extends Iterator implements Countable
|
||||
* Extracts an attribute value from the given element
|
||||
* in the collection. This is useful if elements in the collection
|
||||
* might be objects, arrays or anything else and you need to
|
||||
* get the value independently from that. We use it for filterBy.
|
||||
* get the value independently from that. We use it for `filter`.
|
||||
*
|
||||
* @param array|object $item
|
||||
* @param string $attribute
|
||||
@@ -516,69 +521,77 @@ class Collection extends Iterator implements Countable
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the elements by a given callback
|
||||
* Groups the elements by a given field or callback function
|
||||
*
|
||||
* @param \Closure $callback
|
||||
* @return \Kirby\Toolkit\Collection A new collection with an element for each group and a sub collection in each group
|
||||
* @throws \Exception
|
||||
* @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(Closure $callback)
|
||||
public function group($field, bool $i = true)
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
foreach ($this->data as $key => $item) {
|
||||
// group by field name
|
||||
if (is_string($field) === true) {
|
||||
return $this->group(function ($item) use ($field, $i) {
|
||||
$value = $this->getAttribute($item, $field);
|
||||
|
||||
// get the value to group by
|
||||
$value = $callback($item);
|
||||
// ignore upper/lowercase for group names
|
||||
return $i === true ? Str::lower($value) : $value;
|
||||
});
|
||||
}
|
||||
|
||||
// make sure that there's always a proper value to group by
|
||||
if (!$value) {
|
||||
throw new Exception('Invalid grouping value for key: ' . $key);
|
||||
}
|
||||
// group via callback function
|
||||
if (is_callable($field) === true) {
|
||||
$groups = [];
|
||||
|
||||
// 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');
|
||||
} elseif (is_object($value) === true) {
|
||||
if (method_exists($value, '__toString') === false) {
|
||||
foreach ($this->data as $key => $item) {
|
||||
|
||||
// get the value to group by
|
||||
$value = $field($item);
|
||||
|
||||
// make sure that there's always a proper value to group by
|
||||
if (!$value) {
|
||||
throw new Exception('Invalid grouping value for key: ' . $key);
|
||||
}
|
||||
|
||||
// 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');
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($groups[$value]) === false) {
|
||||
// create a new entry for the group if it does not exist yet
|
||||
$groups[$value] = new static([$key => $item]);
|
||||
} else {
|
||||
$value = (string)$value;
|
||||
// add the element to an existing group
|
||||
$groups[$value]->set($key, $item);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($groups[$value]) === false) {
|
||||
// create a new entry for the group if it does not exist yet
|
||||
$groups[$value] = new static([$key => $item]);
|
||||
} else {
|
||||
// add the element to an existing group
|
||||
$groups[$value]->set($key, $item);
|
||||
}
|
||||
return new Collection($groups);
|
||||
}
|
||||
|
||||
return new Collection($groups);
|
||||
throw new Exception('Can only group by string values or by providing a callback function');
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups the elements by a given field
|
||||
* Alias for `Kirby\Toolkit\Collection::group`
|
||||
*
|
||||
* @param string $field
|
||||
* @param string|Closure $field
|
||||
* @param bool $i
|
||||
* @return \Kirby\Toolkit\Collection A new collection with an element for each group and a sub collection in each group
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function groupBy($field, bool $i = true)
|
||||
public function groupBy(...$args)
|
||||
{
|
||||
if (is_string($field) === false) {
|
||||
throw new Exception('Cannot group by non-string values. Did you mean to call group()?');
|
||||
}
|
||||
|
||||
return $this->group(function ($item) use ($field, $i) {
|
||||
$value = $this->getAttribute($item, $field);
|
||||
|
||||
// ignore upper/lowercase for group names
|
||||
return $i === true ? Str::lower($value) : $value;
|
||||
});
|
||||
return $this->group(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -799,7 +812,7 @@ class Collection extends Iterator implements Countable
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a combination of filterBy, sortBy, not
|
||||
* Runs a combination of filter, sort, not,
|
||||
* offset, limit and paginate on the collection.
|
||||
* Any part of the query is optional.
|
||||
*
|
||||
@@ -814,10 +827,17 @@ class Collection extends Iterator implements Countable
|
||||
$result = $result->not(...$arguments['not']);
|
||||
}
|
||||
|
||||
if (isset($arguments['filterBy']) === true) {
|
||||
foreach ($arguments['filterBy'] as $filter) {
|
||||
if (isset($filter['field']) === true && isset($filter['value']) === true) {
|
||||
$result = $result->filterBy($filter['field'], $filter['operator'] ?? '==', $filter['value']);
|
||||
if ($filters = $arguments['filterBy'] ?? $arguments['filter'] ?? null) {
|
||||
foreach ($filters as $filter) {
|
||||
if (
|
||||
isset($filter['field']) === true &&
|
||||
isset($filter['value']) === true
|
||||
) {
|
||||
$result = $result->filter(
|
||||
$filter['field'],
|
||||
$filter['operator'] ?? '==',
|
||||
$filter['value']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,18 +850,18 @@ class Collection extends Iterator implements Countable
|
||||
$result = $result->limit($arguments['limit']);
|
||||
}
|
||||
|
||||
if (isset($arguments['sortBy']) === true) {
|
||||
if (is_array($arguments['sortBy'])) {
|
||||
$sort = explode(' ', implode(' ', $arguments['sortBy']));
|
||||
if ($sort = $arguments['sortBy'] ?? $arguments['sort'] ?? null) {
|
||||
if (is_array($sort)) {
|
||||
$sort = explode(' ', implode(' ', $sort));
|
||||
} else {
|
||||
// if there are commas in the sortBy argument, removes it
|
||||
if (Str::contains($arguments['sortBy'], ',') === true) {
|
||||
$arguments['sortBy'] = Str::replace($arguments['sortBy'], ',', '');
|
||||
// if there are commas in the sort argument, removes it
|
||||
if (Str::contains($sort, ',') === true) {
|
||||
$sort = Str::replace($sort, ',', '');
|
||||
}
|
||||
|
||||
$sort = explode(' ', $arguments['sortBy']);
|
||||
$sort = explode(' ', $sort);
|
||||
}
|
||||
$result = $result->sortBy(...$sort);
|
||||
$result = $result->sort(...$sort);
|
||||
}
|
||||
|
||||
if (isset($arguments['paginate']) === true) {
|
||||
@@ -924,17 +944,17 @@ class Collection extends Iterator implements Countable
|
||||
/**
|
||||
* Get sort arguments from a string
|
||||
*
|
||||
* @param string $sortBy
|
||||
* @param string $sort
|
||||
* @return array
|
||||
*/
|
||||
public static function sortArgs(string $sortBy): array
|
||||
public static function sortArgs(string $sort): array
|
||||
{
|
||||
// if there are commas in the sortBy argument, removes it
|
||||
if (Str::contains($sortBy, ',') === true) {
|
||||
$sortBy = Str::replace($sortBy, ',', '');
|
||||
if (Str::contains($sort, ',') === true) {
|
||||
$sort = Str::replace($sort, ',', '');
|
||||
}
|
||||
|
||||
$sortArgs = Str::split($sortBy, ' ');
|
||||
$sortArgs = Str::split($sort, ' ');
|
||||
|
||||
// fill in PHP constants
|
||||
array_walk($sortArgs, function (string &$value) {
|
||||
@@ -954,7 +974,7 @@ class Collection extends Iterator implements Countable
|
||||
* @param int $method The sort flag, SORT_REGULAR, SORT_NUMERIC etc.
|
||||
* @return \Kirby\Toolkit\Collection
|
||||
*/
|
||||
public function sortBy()
|
||||
public function sort()
|
||||
{
|
||||
// there is no need to sort empty collections
|
||||
if (empty($this->data) === true) {
|
||||
@@ -1062,6 +1082,19 @@ class Collection extends Iterator implements Countable
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for `Kirby\Toolkit\Collection::sort`
|
||||
*
|
||||
* @param string|callable $field Field name or value callback to sort by
|
||||
* @param string $direction asc or desc
|
||||
* @param int $method The sort flag, SORT_REGULAR, SORT_NUMERIC etc.
|
||||
* @return Collection
|
||||
*/
|
||||
public function sortBy(...$args)
|
||||
{
|
||||
return $this->sort(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object into an array
|
||||
*
|
||||
|
@@ -554,6 +554,10 @@ class F
|
||||
*/
|
||||
public static function read(string $file)
|
||||
{
|
||||
if (is_file($file) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return @file_get_contents($file);
|
||||
}
|
||||
|
||||
@@ -764,6 +768,18 @@ class F
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all extensions of a given file type
|
||||
* or `null` if the file type is unknown
|
||||
*
|
||||
* @param string $type
|
||||
* @return array|null
|
||||
*/
|
||||
public static function typeToExtensions(string $type): ?array
|
||||
{
|
||||
return static::$types[$type] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzips a zip file
|
||||
*
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Closure;
|
||||
use NumberFormatter;
|
||||
|
||||
/**
|
||||
* Localization class, roughly inspired by VueI18n
|
||||
@@ -43,6 +44,13 @@ class I18n
|
||||
*/
|
||||
public static $fallback = 'en';
|
||||
|
||||
/**
|
||||
* Cache of `NumberFormatter` objects by locale
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $decimalNumberFormatters = [];
|
||||
|
||||
/**
|
||||
* Returns the fallback code
|
||||
*
|
||||
@@ -78,6 +86,25 @@ class I18n
|
||||
return $count === 1 ? 'singular' : 'plural';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a number
|
||||
*
|
||||
* @param int|float $number
|
||||
* @param string $locale
|
||||
* @return string
|
||||
*/
|
||||
public static function formatNumber($number, string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?? static::locale();
|
||||
|
||||
$formatter = static::decimalNumberFormatter($locale);
|
||||
if ($formatter !== null) {
|
||||
$number = $formatter->format($number);
|
||||
}
|
||||
|
||||
return (string)$number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale code
|
||||
*
|
||||
@@ -189,6 +216,24 @@ class I18n
|
||||
return static::$translations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns (and creates) a decimal number formatter for a given locale
|
||||
*
|
||||
* @return \NumberFormatter|null
|
||||
*/
|
||||
protected static function decimalNumberFormatter(string $locale): ?NumberFormatter
|
||||
{
|
||||
if (isset(static::$decimalNumberFormatters[$locale])) {
|
||||
return static::$decimalNumberFormatters[$locale];
|
||||
}
|
||||
|
||||
if (extension_loaded('intl') !== true || class_exists('NumberFormatter') !== true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return static::$decimalNumberFormatters[$locale] = new NumberFormatter($locale, NumberFormatter::DECIMAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates amounts
|
||||
*
|
||||
@@ -202,10 +247,13 @@ class I18n
|
||||
* @param string $key
|
||||
* @param int $count
|
||||
* @param string $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)
|
||||
public static function translateCount(string $key, int $count, string $locale = null, bool $formatNumber = true)
|
||||
{
|
||||
$locale = $locale ?? static::locale();
|
||||
|
||||
$translation = static::translate($key, null, $locale);
|
||||
|
||||
if ($translation === null) {
|
||||
@@ -226,6 +274,10 @@ class I18n
|
||||
}
|
||||
}
|
||||
|
||||
if ($formatNumber === true) {
|
||||
$count = static::formatNumber($count, $locale);
|
||||
}
|
||||
|
||||
return str_replace('{{ count }}', $count, $message);
|
||||
}
|
||||
}
|
||||
|
101
kirby/src/Toolkit/Locale.php
Executable file
101
kirby/src/Toolkit/Locale.php
Executable file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* PHP locale handling
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Lukas Bestle <lukas@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier GmbH
|
||||
* @license https://opensource.org/licenses/MIT
|
||||
*/
|
||||
class Locale
|
||||
{
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// list of all possible constant names
|
||||
$constantNames = [
|
||||
'LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY',
|
||||
'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES'
|
||||
];
|
||||
|
||||
// build an associative array with the locales
|
||||
// that are actually supported on this system
|
||||
$constants = [];
|
||||
foreach ($constantNames as $name) {
|
||||
if (defined($name) === true) {
|
||||
$constants[constant($name)] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
// replace the keys in the locale data array with the locale names
|
||||
$return = [];
|
||||
foreach ($locale as $key => $value) {
|
||||
if (isset($constants[$key]) === true) {
|
||||
// the key is a valid constant,
|
||||
// replace it with its string representation
|
||||
$return[$constants[$key]] = $value;
|
||||
} else {
|
||||
// not found, keep it as-is
|
||||
$return[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a locale string or an array with constant or
|
||||
* string keys to a normalized constant => value array
|
||||
*
|
||||
* @param array|string $locale
|
||||
* @return array
|
||||
*/
|
||||
public static function normalize($locale): array
|
||||
{
|
||||
if (is_array($locale)) {
|
||||
// replace string constant keys with the constant values
|
||||
$convertedLocale = [];
|
||||
foreach ($locale as $key => $value) {
|
||||
if (is_string($key) === true && Str::startsWith($key, 'LC_') === true) {
|
||||
$key = constant($key);
|
||||
}
|
||||
|
||||
$convertedLocale[$key] = $value;
|
||||
}
|
||||
|
||||
return $convertedLocale;
|
||||
} elseif (is_string($locale)) {
|
||||
return [LC_ALL => $locale];
|
||||
} else {
|
||||
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($locale): void
|
||||
{
|
||||
$locale = static::normalize($locale);
|
||||
|
||||
foreach ($locale as $key => $value) {
|
||||
setlocale($key, $value);
|
||||
}
|
||||
}
|
||||
}
|
@@ -126,48 +126,30 @@ class Pagination
|
||||
/**
|
||||
* Getter for the current page
|
||||
*
|
||||
* @deprecated 3.3.0 Setter is no longer supported, use $pagination->clone()
|
||||
* @return int
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function page(int $page = null): int
|
||||
public function page(): int
|
||||
{
|
||||
if ($page !== null) {
|
||||
throw new Exception('$pagination->page() setter is no longer supported, use $pagination->clone()'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the total number of items
|
||||
*
|
||||
* @deprecated 3.3.0 Setter is no longer supported, use $pagination->clone()
|
||||
* @return int
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function total(int $total = null): int
|
||||
public function total(): int
|
||||
{
|
||||
if ($total !== null) {
|
||||
throw new Exception('$pagination->total() setter is no longer supported, use $pagination->clone()'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for the number of items per page
|
||||
*
|
||||
* @deprecated 3.3.0 Setter is no longer supported, use $pagination->clone()
|
||||
* @return int
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function limit(int $limit = null): int
|
||||
public function limit(): int
|
||||
{
|
||||
if ($limit !== null) {
|
||||
throw new Exception('$pagination->limit() setter is no longer supported, use $pagination->clone()'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return $this->limit;
|
||||
}
|
||||
|
||||
|
@@ -289,7 +289,7 @@ 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'));
|
||||
$encoded .= rand(1, 2) == 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';';
|
||||
$encoded .= rand(1, 2) === 1 ? '&#' . $code . ';' : '&#x' . dechex($code) . ';';
|
||||
}
|
||||
|
||||
return $encoded;
|
||||
@@ -957,7 +957,7 @@ class Str
|
||||
* @param mixed $type
|
||||
* @return mixed
|
||||
*/
|
||||
public static function toType($string = null, $type)
|
||||
public static function toType($string, $type)
|
||||
{
|
||||
if (is_string($type) === false) {
|
||||
$type = gettype($type);
|
||||
|
Reference in New Issue
Block a user