Upgrade to 4.4.0
This commit is contained in:
@@ -782,7 +782,7 @@ class App
|
||||
if ($input instanceof Page) {
|
||||
try {
|
||||
$html = $input->render();
|
||||
} catch (ErrorPageException $e) {
|
||||
} catch (ErrorPageException|NotFoundException $e) {
|
||||
return $this->io($e);
|
||||
}
|
||||
|
||||
|
@@ -190,8 +190,19 @@ trait AppErrors
|
||||
protected function getAdditionalWhoopsHandler(): CallbackHandler
|
||||
{
|
||||
return new CallbackHandler(function ($exception, $inspector, $run) {
|
||||
$this->trigger('system.exception', compact('exception'));
|
||||
error_log($exception);
|
||||
$isLogged = true;
|
||||
|
||||
// allow hook to modify whether the exception should be logged
|
||||
$isLogged = $this->apply(
|
||||
'system.exception',
|
||||
compact('exception', 'isLogged'),
|
||||
'isLogged'
|
||||
);
|
||||
|
||||
if ($isLogged !== false) {
|
||||
error_log($exception);
|
||||
}
|
||||
|
||||
return Handler::DONE;
|
||||
});
|
||||
}
|
||||
|
@@ -624,7 +624,7 @@ class File extends ModelWithContent
|
||||
* Page URL and the filename as a more stable
|
||||
* alternative for the media URLs.
|
||||
*/
|
||||
public function previewUrl(): string
|
||||
public function previewUrl(): string|null
|
||||
{
|
||||
$parent = $this->parent();
|
||||
$url = Url::to($this->id());
|
||||
@@ -633,6 +633,12 @@ class File extends ModelWithContent
|
||||
case 'page':
|
||||
$preview = $parent->blueprint()->preview();
|
||||
|
||||
// user has no permission to preview page,
|
||||
// also return null for file preview
|
||||
if ($preview === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// the page has a custom preview setting,
|
||||
// thus the file is only accessible through
|
||||
// the direct media URL
|
||||
|
@@ -315,7 +315,7 @@ class FileRules
|
||||
public static function validMime(File $file, string $mime = null): bool
|
||||
{
|
||||
// make it easier to compare the mime
|
||||
$mime = strtolower($mime);
|
||||
$mime = strtolower($mime ?? '');
|
||||
|
||||
if (empty($mime)) {
|
||||
throw new InvalidArgumentException([
|
||||
|
@@ -7,6 +7,7 @@ use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Http\Router;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use Kirby\Uuid\Uuid;
|
||||
|
||||
/**
|
||||
* The language router is used internally
|
||||
@@ -84,6 +85,27 @@ class LanguageRouter
|
||||
}
|
||||
}
|
||||
|
||||
// Language-specific UUID URLs
|
||||
$routes[] = [
|
||||
'pattern' => '@/(page|file)/(:all)',
|
||||
'method' => 'ALL',
|
||||
'env' => 'site',
|
||||
'action' => function (string $languageCode, string $type, string $id) use ($kirby, $language) {
|
||||
// try to resolve to model, but only from UUID cache;
|
||||
// this ensures that only existing UUIDs can be queried
|
||||
// and attackers can't force Kirby to go through the whole
|
||||
// site index with a non-existing UUID
|
||||
if ($model = Uuid::for($type . '://' . $id)?->model(true)) {
|
||||
return $kirby
|
||||
->response()
|
||||
->redirect($model->url($language->code()));
|
||||
}
|
||||
|
||||
// render the error page
|
||||
return false;
|
||||
}
|
||||
];
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@
|
||||
namespace Kirby\Cms;
|
||||
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
class LanguageRoutes
|
||||
{
|
||||
@@ -29,9 +30,26 @@ class LanguageRoutes
|
||||
'pattern' => $language->pattern(),
|
||||
'method' => 'ALL',
|
||||
'env' => 'site',
|
||||
'action' => function ($path = null) use ($language) {
|
||||
'action' => function ($path = null) use ($kirby, $language) {
|
||||
$result = $language->router()->call($path);
|
||||
|
||||
// redirect secondary-language pages that have
|
||||
// been accessed with non-translated slugs in their path
|
||||
// to their fully translated URL
|
||||
if ($path !== null && $result instanceof Page) {
|
||||
if (Str::endsWith($result->url(), $path) === false) {
|
||||
$url = $result->url();
|
||||
$query = $kirby->request()->query()->toString();
|
||||
|
||||
// preserve query across redirect
|
||||
if (empty($query) === false) {
|
||||
$url .= '?' . $query;
|
||||
}
|
||||
|
||||
return $kirby->response()->redirect($url);
|
||||
}
|
||||
}
|
||||
|
||||
// explicitly test for null as $result can
|
||||
// contain falsy values that should still be returned
|
||||
if ($result !== null) {
|
||||
|
@@ -22,7 +22,7 @@ use Throwable;
|
||||
*/
|
||||
class License
|
||||
{
|
||||
protected const HISTORY = [
|
||||
public const HISTORY = [
|
||||
'3' => '2019-02-05',
|
||||
'4' => '2023-11-28'
|
||||
];
|
||||
|
@@ -43,8 +43,16 @@ abstract class ModelPermissions
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
public function can(string $action): bool
|
||||
{
|
||||
/**
|
||||
* Returns whether the current user is allowed to do
|
||||
* a certain action on the model
|
||||
*
|
||||
* @param bool $default Will be returned if $action does not exist
|
||||
*/
|
||||
public function can(
|
||||
string $action,
|
||||
bool $default = false
|
||||
): bool {
|
||||
$user = $this->user->id();
|
||||
$role = $this->user->role()->id();
|
||||
|
||||
@@ -95,12 +103,20 @@ abstract class ModelPermissions
|
||||
}
|
||||
}
|
||||
|
||||
return $this->permissions->for($this->category, $action);
|
||||
return $this->permissions->for($this->category, $action, $default);
|
||||
}
|
||||
|
||||
public function cannot(string $action): bool
|
||||
{
|
||||
return $this->can($action) === false;
|
||||
/**
|
||||
* Returns whether the current user is not allowed to do
|
||||
* a certain action on the model
|
||||
*
|
||||
* @param bool $default Will be returned if $action does not exist
|
||||
*/
|
||||
public function cannot(
|
||||
string $action,
|
||||
bool $default = true
|
||||
): bool {
|
||||
return $this->can($action, !$default) === false;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
|
@@ -180,7 +180,7 @@ class PageBlueprint extends Blueprint
|
||||
return $this->model->toString($preview);
|
||||
}
|
||||
|
||||
return $preview;
|
||||
return $this->model->permissions()->can('preview', true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -110,18 +110,21 @@ class Permissions
|
||||
}
|
||||
}
|
||||
|
||||
public function for(string $category = null, string $action = null): bool
|
||||
{
|
||||
public function for(
|
||||
string|null $category = null,
|
||||
string|null $action = null,
|
||||
bool $default = false
|
||||
): bool {
|
||||
if ($action === null) {
|
||||
if ($this->hasCategory($category) === false) {
|
||||
return false;
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->actions[$category];
|
||||
}
|
||||
|
||||
if ($this->hasAction($category, $action) === false) {
|
||||
return false;
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->actions[$category][$action];
|
||||
|
@@ -51,6 +51,6 @@ class SiteBlueprint extends Blueprint
|
||||
return $this->model->toString($preview);
|
||||
}
|
||||
|
||||
return $preview;
|
||||
return $this->model->permissions()->can('preview', true);
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Kirby\Filesystem;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Language;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
@@ -28,44 +30,32 @@ use Kirby\Toolkit\Str;
|
||||
*/
|
||||
class Filename
|
||||
{
|
||||
/**
|
||||
* List of all applicable attributes
|
||||
*/
|
||||
protected array $attributes;
|
||||
|
||||
/**
|
||||
* The sanitized file extension
|
||||
*/
|
||||
protected string $extension;
|
||||
|
||||
/**
|
||||
* The source original filename
|
||||
*/
|
||||
protected string $filename;
|
||||
|
||||
/**
|
||||
* The sanitized file name
|
||||
*/
|
||||
protected string $name;
|
||||
|
||||
/**
|
||||
* The template for the final name
|
||||
*/
|
||||
protected string $template;
|
||||
|
||||
/**
|
||||
* Creates a new Filename object
|
||||
*
|
||||
* @param string $template for the final name
|
||||
* @param array $attributes List of all applicable attributes
|
||||
*/
|
||||
public function __construct(string $filename, string $template, array $attributes = [])
|
||||
{
|
||||
$this->filename = $filename;
|
||||
$this->template = $template;
|
||||
$this->attributes = $attributes;
|
||||
$this->extension = $this->sanitizeExtension(
|
||||
public function __construct(
|
||||
protected string $filename,
|
||||
protected string $template,
|
||||
protected array $attributes = []
|
||||
) {
|
||||
$this->name = $this->sanitizeName($filename);
|
||||
$this->extension = $this->sanitizeExtension(
|
||||
$attributes['format'] ??
|
||||
pathinfo($filename, PATHINFO_EXTENSION)
|
||||
);
|
||||
$this->name = $this->sanitizeName($filename);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,7 +232,24 @@ class Filename
|
||||
*/
|
||||
protected function sanitizeName(string $name): string
|
||||
{
|
||||
return F::safeBasename($name);
|
||||
// temporarily store language rules
|
||||
$rules = Str::$language;
|
||||
$kirby = App::instance(null, true);
|
||||
|
||||
// if current user, add rules for their language to `Str` class
|
||||
if ($user = $kirby?->user()) {
|
||||
Str::$language = [
|
||||
...Str::$language,
|
||||
...Language::loadRules($user->language())];
|
||||
}
|
||||
|
||||
// sanitize name
|
||||
$name = F::safeBasename($this->filename);
|
||||
|
||||
// restore language rules
|
||||
Str::$language = $rules;
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -162,11 +162,18 @@ class Validations
|
||||
*/
|
||||
public static function pattern(Field|FieldClass $field, mixed $value): bool
|
||||
{
|
||||
if ($field->isEmpty($value) === false && $field->pattern() !== null) {
|
||||
if (V::match($value, '/' . $field->pattern() . '/i') === false) {
|
||||
throw new InvalidArgumentException(
|
||||
V::message('match')
|
||||
);
|
||||
if ($field->isEmpty($value) === false) {
|
||||
if ($pattern = $field->pattern()) {
|
||||
// ensure that that pattern needs to match the whole
|
||||
// input value from start to end, not just a partial match
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/pattern#overview
|
||||
$pattern = '^(?:' . $pattern . ')$';
|
||||
|
||||
if (V::match($value, '/' . $pattern . '/i') === false) {
|
||||
throw new InvalidArgumentException(
|
||||
V::message('match')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -602,13 +602,7 @@ class Environment
|
||||
*/
|
||||
protected function detectRequestUri(string|null $requestUri = null): Uri
|
||||
{
|
||||
// make sure the URL parser works properly when there's a
|
||||
// colon in the request URI but the URI is relative
|
||||
if (Url::isAbsolute($requestUri) === false) {
|
||||
$requestUri = 'https://getkirby.com' . $requestUri;
|
||||
}
|
||||
|
||||
$uri = new Uri($requestUri);
|
||||
$uri = new Uri($requestUri ?? '');
|
||||
|
||||
// create the URI object as a combination of base uri parts
|
||||
// and the parts from REQUEST_URI
|
||||
|
@@ -88,7 +88,16 @@ class Uri
|
||||
public function __construct(array|string $props = [], array $inject = [])
|
||||
{
|
||||
if (is_string($props) === true) {
|
||||
$props = parse_url($props);
|
||||
// make sure the URL parser works properly when there's a
|
||||
// colon in the string but the string is a relative URL
|
||||
if (Url::isAbsolute($props) === false) {
|
||||
$props = 'https://getkirby.com/' . $props;
|
||||
$props = parse_url($props);
|
||||
unset($props['scheme'], $props['host']);
|
||||
} else {
|
||||
$props = parse_url($props);
|
||||
}
|
||||
|
||||
$props['username'] = $props['user'] ?? null;
|
||||
$props['password'] = $props['pass'] ?? null;
|
||||
|
||||
|
@@ -352,12 +352,15 @@ class File extends Model
|
||||
$id = $this->model->id();
|
||||
|
||||
if (empty($params['model']) === false) {
|
||||
$parent = $this->model->parent();
|
||||
$parent = $this->model->parent();
|
||||
$absolute = $parent !== $params['model'];
|
||||
|
||||
// if the file belongs to the current parent model,
|
||||
// store only name as ID to keep its path relative to the model
|
||||
$id = $parent === $params['model'] ? $name : $id;
|
||||
$absolute = $parent !== $params['model'];
|
||||
$id = match ($absolute) {
|
||||
true => $id,
|
||||
false => $name
|
||||
};
|
||||
}
|
||||
|
||||
$params['text'] ??= '{{ file.filename }}';
|
||||
@@ -399,6 +402,7 @@ class File extends Model
|
||||
'template' => $file->template(),
|
||||
'type' => $file->type(),
|
||||
'url' => $file->url(),
|
||||
'uuid' => fn () => $file->uuid()?->toString(),
|
||||
],
|
||||
'preview' => [
|
||||
'focusable' => $this->isFocusable(),
|
||||
|
@@ -335,7 +335,7 @@ abstract class Model
|
||||
'link' => $this->url(true),
|
||||
'sortable' => true,
|
||||
'text' => $this->model->toSafeString($params['text'] ?? false),
|
||||
'uuid' => $this->model->uuid()?->toString() ?? $this->model->id(),
|
||||
'uuid' => $this->model->uuid()?->toString()
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -339,6 +339,7 @@ class Page extends Model
|
||||
'previewUrl' => $page->previewUrl(),
|
||||
'status' => $page->status(),
|
||||
'title' => $page->title()->toString(),
|
||||
'uuid' => fn () => $page->uuid()?->toString(),
|
||||
],
|
||||
'status' => function () use ($page) {
|
||||
if ($status = $page->status()) {
|
||||
|
@@ -71,6 +71,7 @@ class Site extends Model
|
||||
'link' => $this->url(true),
|
||||
'previewUrl' => $this->model->previewUrl(),
|
||||
'title' => $this->model->title()->toString(),
|
||||
'uuid' => fn () => $this->model->uuid()?->toString(),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
@@ -237,6 +237,7 @@ class User extends Model
|
||||
'name' => $user->name()->toString(),
|
||||
'role' => $user->role()->title(),
|
||||
'username' => $user->username(),
|
||||
'uuid' => fn () => $user->uuid()?->toString()
|
||||
]
|
||||
]
|
||||
);
|
||||
|
@@ -738,12 +738,18 @@ class A
|
||||
/**
|
||||
* Returns a number of random elements from an array,
|
||||
* either in original or shuffled order
|
||||
*
|
||||
* @throws \Exception When $count is larger than array length
|
||||
*/
|
||||
public static function random(
|
||||
array $array,
|
||||
int $count = 1,
|
||||
bool $shuffle = false
|
||||
): array {
|
||||
if ($count > count($array)) {
|
||||
throw new InvalidArgumentException('$count is larger than available array items');
|
||||
}
|
||||
|
||||
if ($shuffle === true) {
|
||||
return array_slice(self::shuffle($array), 0, $count);
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ use Kirby\Cms\Site;
|
||||
use Kirby\Cms\User;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
@@ -217,7 +218,13 @@ abstract class Uuid
|
||||
return (static::$generator)($length);
|
||||
}
|
||||
|
||||
if (App::instance()->option('content.uuid') === 'uuid-v4') {
|
||||
$option = App::instance()->option('content.uuid');
|
||||
|
||||
if (is_array($option) === true) {
|
||||
$option = $option['format'] ?? null;
|
||||
}
|
||||
|
||||
if ($option === 'uuid-v4') {
|
||||
return Str::uuid();
|
||||
}
|
||||
|
||||
@@ -332,6 +339,10 @@ abstract class Uuid
|
||||
}
|
||||
|
||||
if ($lazy === false) {
|
||||
if (App::instance()->option('content.uuid.index') === false) {
|
||||
throw new NotFoundException('Model for UUID ' . $this->uri->toString() . ' could not be found without searching in the site index');
|
||||
}
|
||||
|
||||
if ($this->model = $this->findByIndex()) {
|
||||
// lazily fill cache by writing to cache
|
||||
// whenever looked up from index to speed
|
||||
|
Reference in New Issue
Block a user