1
0

downgrade to kirby v3

This commit is contained in:
Philip Wagner
2024-09-01 10:47:15 +02:00
parent a4b2aece7b
commit af86acb7a1
1085 changed files with 54743 additions and 65042 deletions

View File

@@ -2,18 +2,13 @@
namespace Kirby\Cms;
use Closure;
use Exception;
use Kirby\Content\Field;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Exception\PermissionException;
use Kirby\Filesystem\Dir;
use Kirby\Filesystem\F;
use Kirby\Panel\User as Panel;
use Kirby\Session\Session;
use Kirby\Toolkit\Str;
use SensitiveParameter;
/**
* The `$user` object represents a
@@ -35,65 +30,80 @@ class User extends ModelWithContent
public const CLASS_ALIAS = 'user';
/**
* All registered user methods
* @todo Remove when support for PHP 8.2 is dropped
* @var UserBlueprint
*/
public static array $methods = [];
protected $blueprint;
/**
* @var array
*/
protected $credentials;
/**
* @var string
*/
protected $email;
/**
* @var string
*/
protected $hash;
/**
* @var string
*/
protected $id;
/**
* @var array|null
*/
protected $inventory;
/**
* @var string
*/
protected $language;
/**
* All registered user methods
*
* @var array
*/
public static $methods = [];
/**
* Registry with all User models
*
* @var array
*/
public static array $models = [];
protected UserBlueprint|null $blueprint = null;
protected array $credentials;
protected string|null $email;
protected string $hash;
protected string $id;
protected array|null $inventory = null;
protected string|null $language;
protected Field|string|null $name;
protected string|null $password;
protected Role|string|null $role;
public static $models = [];
/**
* Creates a new User object
* @var \Kirby\Cms\Field
*/
public function __construct(array $props)
{
// helper function to easily edit values (if not null)
// before assigning them to their properties
$set = function (string $key, Closure $callback) use ($props) {
if ($value = $props[$key] ?? null) {
$value = $callback($value);
}
protected $name;
return $value;
};
/**
* @var string
*/
protected $password;
// if no ID passed, generate one;
// do so before calling parent constructor
// so it also gets stored in propertyData prop
$props['id'] ??= $this->createId();
parent::__construct($props);
$this->id = $props['id'];
$this->email = $set('email', fn ($email) => Str::lower(trim($email)));
$this->language = $set('language', fn ($language) => trim($language));
$this->name = $set('name', fn ($name) => trim(strip_tags($name)));
$this->password = $props['password'] ?? null;
$this->role = $set('role', fn ($role) => Str::lower(trim($role)));
$this->setBlueprint($props['blueprint'] ?? null);
$this->setFiles($props['files'] ?? null);
}
/**
* The user role
*
* @var string
*/
protected $role;
/**
* Modified getter to also return fields
* from the content
*
* @param string $method
* @param array $arguments
* @return mixed
*/
public function __call(string $method, array $arguments = []): mixed
public function __call(string $method, array $arguments = [])
{
// public property access
if (isset($this->$method) === true) {
@@ -109,9 +119,22 @@ class User extends ModelWithContent
return $this->content()->get($method);
}
/**
* Creates a new User object
*
* @param array $props
*/
public function __construct(array $props)
{
// TODO: refactor later to avoid redundant prop setting
$this->setProperty('id', $props['id'] ?? $this->createId(), true);
$this->setProperties($props);
}
/**
* Improved `var_dump` output
* @codeCoverageIgnore
*
* @return array
*/
public function __debugInfo(): array
{
@@ -124,34 +147,45 @@ class User extends ModelWithContent
/**
* Returns the url to the api endpoint
*
* @internal
* @param bool $relative
* @return string
*/
public function apiUrl(bool $relative = false): string
{
if ($relative === true) {
return 'users/' . $this->id();
} else {
return $this->kirby()->url('api') . '/users/' . $this->id();
}
return $this->kirby()->url('api') . '/users/' . $this->id();
}
/**
* Returns the File object for the avatar or null
*
* @return \Kirby\Cms\File|null
*/
public function avatar(): File|null
public function avatar()
{
return $this->files()->template('avatar')->first();
}
/**
* Returns the UserBlueprint object
*
* @return \Kirby\Cms\Blueprint
*/
public function blueprint(): UserBlueprint
public function blueprint()
{
if (is_a($this->blueprint, 'Kirby\Cms\Blueprint') === true) {
return $this->blueprint;
}
try {
return $this->blueprint ??= UserBlueprint::factory('users/' . $this->role(), 'users/default', $this);
} catch (Exception) {
return $this->blueprint ??= new UserBlueprint([
return $this->blueprint = UserBlueprint::factory('users/' . $this->role(), 'users/default', $this);
} catch (Exception $e) {
return $this->blueprint = new UserBlueprint([
'model' => $this,
'name' => 'default',
'title' => 'Default',
@@ -161,13 +195,14 @@ class User extends ModelWithContent
/**
* Prepares the content for the write method
*
* @internal
* @param array $data
* @param string $languageCode|null Not used so far
* @return array
*/
public function contentFileData(
array $data,
string|null $languageCode = null
): array {
public function contentFileData(array $data, string $languageCode = null): array
{
// remove stuff that has nothing to do in the text files
unset(
$data['email'],
@@ -184,13 +219,10 @@ class User extends ModelWithContent
* Filename for the content file
*
* @internal
* @deprecated 4.0.0
* @todo Remove in v5
* @codeCoverageIgnore
* @return string
*/
public function contentFileName(): string
{
Helpers::deprecated('The internal $model->contentFileName() method has been deprecated. Please let us know via a GitHub issue if you need this method and tell us your use case.', 'model-content-file');
return 'user';
}
@@ -201,29 +233,33 @@ class User extends ModelWithContent
/**
* Returns the user email address
*
* @return string
*/
public function email(): string|null
public function email(): ?string
{
return $this->email ??= $this->credentials()['email'] ?? null;
}
/**
* Checks if the user exists
*
* @return bool
*/
public function exists(): bool
{
return $this->storage()->exists(
'published',
'default'
);
return is_file($this->contentFile('default')) === true;
}
/**
* Constructs a User object and also
* takes User models into account.
*
* @internal
* @param mixed $props
* @return static
*/
public static function factory(mixed $props): static
public static function factory($props)
{
if (empty($props['model']) === false) {
return static::model($props['model'], $props);
@@ -235,12 +271,13 @@ class User extends ModelWithContent
/**
* Hashes the user's password unless it is `null`,
* which will leave it as `null`
*
* @internal
* @param string|null $password
* @return string|null
*/
public static function hashPassword(
#[SensitiveParameter]
string $password = null
): string|null {
public static function hashPassword($password): ?string
{
if ($password !== null) {
$password = password_hash($password, PASSWORD_DEFAULT);
}
@@ -250,6 +287,8 @@ class User extends ModelWithContent
/**
* Returns the user id
*
* @return string
*/
public function id(): string
{
@@ -259,6 +298,8 @@ class User extends ModelWithContent
/**
* Returns the inventory of files
* children and content files
*
* @return array
*/
public function inventory(): array
{
@@ -278,6 +319,9 @@ class User extends ModelWithContent
/**
* Compares the current object with the given user object
*
* @param \Kirby\Cms\User|null $user
* @return bool
*/
public function is(User $user = null): bool
{
@@ -290,6 +334,8 @@ class User extends ModelWithContent
/**
* Checks if this user has the admin role
*
* @return bool
*/
public function isAdmin(): bool
{
@@ -299,14 +345,18 @@ class User extends ModelWithContent
/**
* Checks if the current user is the virtual
* Kirby user
*
* @return bool
*/
public function isKirby(): bool
{
return $this->isAdmin() && $this->id() === 'kirby';
return $this->email() === 'kirby@getkirby.com';
}
/**
* Checks if the current user is this user
*
* @return bool
*/
public function isLoggedIn(): bool
{
@@ -316,16 +366,19 @@ class User extends ModelWithContent
/**
* Checks if the user is the last one
* with the admin role
*
* @return bool
*/
public function isLastAdmin(): bool
{
return
$this->role()->isAdmin() === true &&
$this->kirby()->users()->filter('role', 'admin')->count() <= 1;
return $this->role()->isAdmin() === true &&
$this->kirby()->users()->filter('role', 'admin')->count() <= 1;
}
/**
* Checks if the user is the last user
*
* @return bool
*/
public function isLastUser(): bool
{
@@ -335,32 +388,33 @@ class User extends ModelWithContent
/**
* Checks if the current user is the virtual
* Nobody user
*
* @return bool
*/
public function isNobody(): bool
{
return $this->role()->id() === 'nobody' && $this->id() === 'nobody';
return $this->email() === 'nobody@getkirby.com';
}
/**
* Returns the user language
*
* @return string
*/
public function language(): string
{
return $this->language ??=
$this->credentials()['language'] ??
$this->kirby()->panelLanguage();
return $this->language ??= $this->credentials()['language'] ?? $this->kirby()->panelLanguage();
}
/**
* Logs the user in
*
* @param string $password
* @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in
* @return bool
*/
public function login(
#[SensitiveParameter]
string $password,
$session = null
): bool {
public function login(string $password, $session = null): bool
{
$this->validatePassword($password);
$this->loginPasswordless($session);
@@ -371,43 +425,33 @@ class User extends ModelWithContent
* Logs the user in without checking the password
*
* @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in
* @return void
*/
public function loginPasswordless(
Session|array|null $session = null
): void {
if ($this->id() === 'kirby') {
throw new PermissionException('The almighty user "kirby" cannot be used for login, only for raising permissions in code via `$kirby->impersonate()`');
}
public function loginPasswordless($session = null): void
{
$kirby = $this->kirby();
$kirby = $this->kirby();
$session = $this->sessionFromOptions($session);
$kirby->trigger(
'user.login:before',
['user' => $this, 'session' => $session]
);
$kirby->trigger('user.login:before', ['user' => $this, 'session' => $session]);
$session->regenerateToken(); // privilege change
$session->data()->set('kirby.userId', $this->id());
if ($this->passwordTimestamp() !== null) {
$session->data()->set('kirby.loginTimestamp', time());
}
$this->kirby()->auth()->setUser($this);
$kirby->auth()->setUser($this);
$kirby->trigger(
'user.login:after',
['user' => $this, 'session' => $session]
);
$kirby->trigger('user.login:after', ['user' => $this, 'session' => $session]);
}
/**
* Logs the user out
*
* @param \Kirby\Session\Session|array|null $session Session options or session object to unset the user in
* @return void
*/
public function logout(Session|array|null $session = null): void
public function logout($session = null): void
{
$kirby = $this->kirby();
$session = $this->sessionFromOptions($session);
@@ -436,7 +480,9 @@ class User extends ModelWithContent
/**
* Returns the root to the media folder for the user
*
* @internal
* @return string
*/
public function mediaRoot(): string
{
@@ -445,7 +491,9 @@ class User extends ModelWithContent
/**
* Returns the media url for the user object
*
* @internal
* @return string
*/
public function mediaUrl(): string
{
@@ -454,14 +502,18 @@ class User extends ModelWithContent
/**
* Creates a user model if it has been registered
*
* @internal
* @param string $name
* @param array $props
* @return \Kirby\Cms\User
*/
public static function model(string $name, array $props = []): static
public static function model(string $name, array $props = [])
{
if ($class = (static::$models[$name] ?? null)) {
$object = new $class($props);
if ($object instanceof self) {
if (is_a($object, 'Kirby\Cms\User') === true) {
return $object;
}
}
@@ -471,36 +523,47 @@ class User extends ModelWithContent
/**
* Returns the last modification date of the user
*
* @param string $format
* @param string|null $handler
* @param string|null $languageCode
* @return int|string
*/
public function modified(
string $format = 'U',
string|null $handler = null,
string|null $languageCode = null
): int|string|false {
$modifiedContent = $this->storage()->modified('published', $languageCode);
public function modified(string $format = 'U', string $handler = null, string $languageCode = null)
{
$modifiedContent = F::modified($this->contentFile($languageCode));
$modifiedIndex = F::modified($this->root() . '/index.php');
$modifiedTotal = max([$modifiedContent, $modifiedIndex]);
$handler ??= $this->kirby()->option('date.handler', 'date');
return Str::date($modifiedTotal, $format, $handler);
}
/**
* Returns the user's name
*
* @return \Kirby\Cms\Field
*/
public function name(): Field
public function name()
{
if (is_string($this->name) === true) {
return new Field($this, 'name', $this->name);
}
return $this->name ??= new Field($this, 'name', $this->credentials()['name'] ?? null);
if ($this->name !== null) {
return $this->name;
}
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(): Field
public function nameOrEmail()
{
$name = $this->name();
return $name->isNotEmpty() ? $name : new Field($this, 'email', $this->email());
@@ -508,9 +571,11 @@ class User extends ModelWithContent
/**
* Create a dummy nobody
*
* @internal
* @return static
*/
public static function nobody(): static
public static function nobody()
{
return new static([
'email' => 'nobody@getkirby.com',
@@ -520,27 +585,35 @@ class User extends ModelWithContent
/**
* Returns the panel info object
*
* @return \Kirby\Panel\User
*/
public function panel(): Panel
public function panel()
{
return new Panel($this);
}
/**
* Returns the encrypted user password
*
* @return string|null
*/
public function password(): string|null
public function password(): ?string
{
return $this->password ??= $this->readPassword();
if ($this->password !== null) {
return $this->password;
}
return $this->password = $this->readPassword();
}
/**
* Returns the timestamp when the password
* was last changed
*/
public function passwordTimestamp(): int|null
public function passwordTimestamp(): ?int
{
$file = $this->secretsFile();
$file = $this->passwordFile();
// ensure we have the latest information
// to prevent cache attacks
@@ -554,31 +627,42 @@ class User extends ModelWithContent
return filemtime($file);
}
public function permissions(): UserPermissions
/**
* @return \Kirby\Cms\UserPermissions
*/
public function permissions()
{
return new UserPermissions($this);
}
/**
* Returns the user role
*
* @return \Kirby\Cms\Role
*/
public function role(): Role
public function role()
{
if ($this->role instanceof Role) {
if (is_a($this->role, 'Kirby\Cms\Role') === true) {
return $this->role;
}
$name = $this->role ?? $this->credentials()['role'] ?? 'visitor';
$roleName = $this->role ?? $this->credentials()['role'] ?? 'visitor';
return $this->role = $this->kirby()->roles()->find($name) ?? Role::nobody();
if ($role = $this->kirby()->roles()->find($roleName)) {
return $this->role = $role;
}
return $this->role = Role::nobody();
}
/**
* Returns all available roles
* for this user, that can be selected
* by the authenticated user
*
* @return \Kirby\Cms\Roles
*/
public function roles(): Roles
public function roles()
{
$kirby = $this->kirby();
$roles = $kirby->roles();
@@ -587,16 +671,18 @@ class User extends ModelWithContent
$myRole = $roles->filter('id', $this->role()->id());
// if there's an authenticated user …
// admin users can select pretty much any role
if ($kirby->user()?->isAdmin() === true) {
// except if the user is the last admin
if ($this->isLastAdmin() === true) {
// in which case they have to stay admin
return $myRole;
}
if ($user = $kirby->user()) {
// admin users can select pretty much any role
if ($user->isAdmin() === true) {
// except if the user is the last admin
if ($this->isLastAdmin() === true) {
// in which case they have to stay admin
return $myRole;
}
// return all roles for mighty admins
return $roles;
// return all roles for mighty admins
return $roles;
}
}
// any other user can only keep their role
@@ -605,6 +691,8 @@ class User extends ModelWithContent
/**
* The absolute path to the user directory
*
* @return string
*/
public function root(): string
{
@@ -614,27 +702,21 @@ class User extends ModelWithContent
/**
* Returns the UserRules class to
* validate any important action.
*
* @return \Kirby\Cms\UserRules
*/
protected function rules(): UserRules
protected function rules()
{
return new UserRules();
}
/**
* Reads a specific secret from the user secrets file on disk
* @since 4.0.0
*/
public function secret(string $key): mixed
{
return $this->readSecrets()[$key] ?? null;
}
/**
* Sets the Blueprint object
*
* @param array|null $blueprint
* @return $this
*/
protected function setBlueprint(array $blueprint = null): static
protected function setBlueprint(array $blueprint = null)
{
if ($blueprint !== null) {
$blueprint['model'] = $this;
@@ -644,17 +726,92 @@ class User extends ModelWithContent
return $this;
}
/**
* Sets the user email
*
* @param string $email|null
* @return $this
*/
protected function setEmail(string $email = null)
{
if ($email !== null) {
$this->email = Str::lower(trim($email));
}
return $this;
}
/**
* Sets the user id
*
* @param string $id|null
* @return $this
*/
protected function setId(string $id = null)
{
$this->id = $id;
return $this;
}
/**
* Sets the user language
*
* @param string $language|null
* @return $this
*/
protected function setLanguage(string $language = null)
{
$this->language = $language !== null ? trim($language) : null;
return $this;
}
/**
* Sets the user name
*
* @param string $name|null
* @return $this
*/
protected function setName(string $name = null)
{
$this->name = $name !== null ? trim(strip_tags($name)) : null;
return $this;
}
/**
* Sets the user's password hash
*
* @param string $password|null
* @return $this
*/
protected function setPassword(string $password = null)
{
$this->password = $password;
return $this;
}
/**
* Sets the user role
*
* @param string $role|null
* @return $this
*/
protected function setRole(string $role = null)
{
$this->role = $role !== null ? Str::lower(trim($role)) : null;
return $this;
}
/**
* Converts session options into a session object
*
* @param \Kirby\Session\Session|array $session Session options or session object to unset the user in
* @return \Kirby\Session\Session
*/
protected function sessionFromOptions(Session|array|null $session): Session
protected function sessionFromOptions($session)
{
// use passed session options or session object if set
if (is_array($session) === true) {
$session = $this->kirby()->session($session);
} elseif ($session instanceof Session === false) {
} elseif (is_a($session, 'Kirby\Session\Session') === false) {
$session = $this->kirby()->session(['detect' => true]);
}
@@ -663,8 +820,10 @@ class User extends ModelWithContent
/**
* Returns the parent Users collection
*
* @return \Kirby\Cms\Users
*/
protected function siblingsCollection(): Users
protected function siblingsCollection()
{
return $this->kirby()->users();
}
@@ -672,32 +831,36 @@ class User extends ModelWithContent
/**
* Converts the most important user properties
* to an array
*
* @return array
*/
public function toArray(): array
{
return array_merge(parent::toArray(), [
'avatar' => $this->avatar()?->toArray(),
return [
'avatar' => $this->avatar() ? $this->avatar()->toArray() : null,
'content' => $this->content()->toArray(),
'email' => $this->email(),
'id' => $this->id(),
'language' => $this->language(),
'role' => $this->role()->name(),
'username' => $this->username()
]);
];
}
/**
* String template builder
*
* @param string|null $fallback Fallback for tokens in the template that cannot be replaced
* (`null` to keep the original token)
* @param string|null $template
* @param array|null $data
* @param string $fallback Fallback for tokens in the template that cannot be replaced
* @return string
*/
public function toString(
string $template = null,
array $data = [],
string|null $fallback = '',
string $handler = 'template'
): string {
$template ??= $this->email();
public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string
{
if ($template === null) {
$template = $this->email();
}
return parent::toString($template, $data, $fallback, $handler);
}
@@ -705,8 +868,10 @@ class User extends ModelWithContent
* Returns the username
* which is the given name or the email
* as a fallback
*
* @return string|null
*/
public function username(): string|null
public function username(): ?string
{
return $this->name()->or($this->email())->value();
}
@@ -714,14 +879,15 @@ class User extends ModelWithContent
/**
* Compares the given password with the stored one
*
* @param string $password|null
* @return bool
*
* @throws \Kirby\Exception\NotFoundException If the user has no password
* @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid
* or does not match the user password
*/
public function validatePassword(
#[SensitiveParameter]
string $password = null
): bool {
public function validatePassword(string $password = null): bool
{
if (empty($this->password()) === true) {
throw new NotFoundException(['key' => 'user.password.undefined']);
}
@@ -745,21 +911,61 @@ class User extends ModelWithContent
}
/**
* @deprecated 4.0.0 Use `->secretsFile()` instead
* @codeCoverageIgnore
* Returns the path to the password file
*/
protected function passwordFile(): string
{
return $this->secretsFile();
return $this->root() . '/.htpasswd';
}
/**
* Returns the path to the file containing
* all user secrets, including the password
* @since 4.0.0
* Deprecated!
*/
protected function secretsFile(): string
/**
* Returns the full path without leading slash
*
* @todo Remove in 3.8.0
*
* @internal
* @return string
* @codeCoverageIgnore
*/
public function panelPath(): string
{
return $this->root() . '/.htpasswd';
Helpers::deprecated('Cms\User::panelPath() has been deprecated and will be removed in Kirby 3.8.0. Use $user->panel()->path() instead.');
return $this->panel()->path();
}
/**
* Returns prepared data for the panel user picker
*
* @todo Remove in 3.8.0
*
* @param array|null $params
* @return array
* @codeCoverageIgnore
*/
public function panelPickerData(array $params = null): array
{
Helpers::deprecated('Cms\User::panelPickerData() has been deprecated and will be removed in Kirby 3.8.0. Use $user->panel()->pickerData() instead.');
return $this->panel()->pickerData($params);
}
/**
* Returns the url to the editing view
* in the panel
*
* @todo Remove in 3.8.0
*
* @internal
* @param bool $relative
* @return string
* @codeCoverageIgnore
*/
public function panelUrl(bool $relative = false): string
{
Helpers::deprecated('Cms\User::panelUrl() has been deprecated and will be removed in Kirby 3.8.0. Use $user->panel()->url() instead.');
return $this->panel()->url($relative);
}
}