Upgrade to rc5
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user