Upgrade to 4.1.0

This commit is contained in:
Bastian Allgeier
2024-01-30 16:41:06 +01:00
parent 5c44c8fcfd
commit 9345fc1a0b
59 changed files with 678 additions and 274 deletions

View File

@@ -3,7 +3,7 @@
"description": "The Kirby core",
"license": "proprietary",
"type": "kirby-cms",
"version": "4.0.3",
"version": "4.1.0",
"keywords": [
"kirby",
"cms",
@@ -99,6 +99,8 @@
"analyze:composer": "composer validate --strict --no-check-version --no-check-all",
"analyze:phpmd": "phpmd . ansi phpmd.xml.dist --exclude 'dependencies/*,tests/*,vendor/*'",
"analyze:psalm": "psalm",
"bench": "phpbench run --report=aggregate --ref baseline",
"bench:baseline": "phpbench run --report=aggregate --tag baseline",
"build": "./scripts/build",
"ci": [
"@fix",
@@ -106,8 +108,8 @@
"@test"
],
"fix": "php-cs-fixer fix",
"test": "phpunit --stderr",
"test:coverage": "XDEBUG_MODE=coverage phpunit --stderr --coverage-html=tests/coverage",
"test": "phpunit",
"test:coverage": "XDEBUG_MODE=coverage phpunit --coverage-html=tests/coverage",
"zip": "composer archive --format=zip --file=dist"
}
}

2
kirby/composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "977754e23aeb4791a5c4f60c6e18ddc6",
"content-hash": "815a3c3a27039ef652b9619518a6cc9c",
"packages": [
{
"name": "christian-riesen/base32",

View File

@@ -84,9 +84,9 @@ return [
'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag',
'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags',
'kirby\cms\template' => 'Kirby\Template\Template',
'kirby\form\options' => 'Kirby\Options\Options',
'kirby\form\optionsapi' => 'Kirby\Options\OptionsApi',
'kirby\form\optionsquery' => 'Kirby\Options\OptionsQuery',
'kirby\form\options' => 'Kirby\Option\Options',
'kirby\form\optionsapi' => 'Kirby\Option\OptionsApi',
'kirby\form\optionsquery' => 'Kirby\Option\OptionsQuery',
'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir',
'kirby\toolkit\f' => 'Kirby\Filesystem\F',
'kirby\toolkit\file' => 'Kirby\Filesystem\File',

View File

@@ -10,6 +10,7 @@ use Kirby\Panel\ChangesDialog;
use Kirby\Panel\Field;
use Kirby\Panel\PageCreateDialog;
use Kirby\Panel\Panel;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
use Kirby\Uuid\Uuids;

View File

@@ -21,7 +21,7 @@ return [
'license' => [
'code' => $license->code($obfuscated),
'icon' => $status->icon(),
'info' => $status->info($license->renewal('Y-m-d')),
'info' => $status->info($license->renewal('Y-m-d', 'date')),
'theme' => $status->theme(),
'type' => $license->label(),
],

View File

@@ -1,7 +1,9 @@
<?php
use Kirby\Exception\InvalidArgumentException;
use Kirby\Field\FieldOptions;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Escape;
use Kirby\Toolkit\Str;
return [
@@ -52,42 +54,87 @@ return [
return Str::lower($this->default);
},
'options' => function (): array {
return A::map(array_keys($this->options), fn ($key) => [
'value' => $this->options[$key],
'text' => is_string($key) ? $key : null
// resolve options to support manual arrays
// alongside api and query options
$props = FieldOptions::polyfill($this->props);
$options = FieldOptions::factory([
'text' => '{{ item.value }}',
'value' => '{{ item.key }}',
...$props['options']
]);
$options = $options->render($this->model());
if (empty($options) === true) {
return [];
}
$options = match (true) {
// simple array of values
// or value=text (from Options class)
is_numeric($options[0]['value']) ||
$options[0]['value'] === $options[0]['text']
=> A::map($options, fn ($option) => [
'value' => $option['text']
]),
// deprecated: name => value, flipping
// TODO: start throwing in warning in v5
$this->isColor($options[0]['text'])
=> A::map($options, fn ($option) => [
'value' => $option['text'],
// ensure that any HTML in the new text is escaped
'text' => Escape::html($option['value'])
]),
default
=> A::map($options, fn ($option) => [
'value' => $option['value'],
'text' => $option['text']
]),
};
return $options;
}
],
'methods' => [
'isColor' => function (string $value): bool {
return
$this->isHex($value) ||
$this->isRgb($value) ||
$this->isHsl($value);
},
'isHex' => function (string $value): bool {
return preg_match('/^#([\da-f]{3,4}){1,2}$/i', $value) === 1;
},
'isHsl' => function (string $value): bool {
return preg_match('/^hsla?\(\s*(\d{1,3}\.?\d*)(deg|rad|grad|turn)?(?:,|\s)+(\d{1,3})%(?:,|\s)+(\d{1,3})%(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) === 1;
},
'isRgb' => function (string $value): bool {
return preg_match('/^rgba?\(\s*(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) === 1;
},
],
'validations' => [
'color' => function ($value) {
if (empty($value) === true) {
return true;
}
if (
$this->format === 'hex' &&
preg_match('/^#([\da-f]{3,4}){1,2}$/i', $value) !== 1
) {
if ($this->format === 'hex' && $this->isHex($value) === false) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'hex']
]);
}
if (
$this->format === 'rgb' &&
preg_match('/^rgba?\(\s*(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s)+(\d{1,3})(%?)(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) !== 1
) {
if ($this->format === 'rgb' && $this->isRgb($value) === false) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'rgb']
]);
}
if (
$this->format === 'hsl' &&
preg_match('/^hsla?\(\s*(\d{1,3}\.?\d*)(deg|rad|grad|turn)?(?:,|\s)+(\d{1,3})%(?:,|\s)+(\d{1,3})%(?:,|\s|\/)*(\d*(?:\.\d+)?)(%?)\s*\)?$/i', $value) !== 1
) {
if ($this->format === 'hsl' && $this->isHsl($value) === false) {
throw new InvalidArgumentException([
'key' => 'validation.color',
'data' => ['format' => 'hsl']

View File

@@ -173,7 +173,7 @@ return [
},
'form' => function (array $values = []) {
return new Form([
'fields' => $this->attrs['fields'],
'fields' => $this->attrs['fields'] ?? [],
'values' => $values,
'model' => $this->model
]);

View File

@@ -257,7 +257,7 @@ return function (App $app) {
try {
return Structure::factory(
Data::decode($field->value, 'yaml'),
['parent' => $field->parent()]
['parent' => $field->parent(), 'field' => $field]
);
} catch (Exception) {
$message = 'Invalid structure data for "' . $field->key() . '" field';

View File

@@ -55,7 +55,7 @@ return [
'parent' => function () {
return $this->parentModel();
},
'files' => function () {
'models' => function () {
if ($this->query !== null) {
$files = $this->parent->query($this->query, Files::class) ?? new Files([]);
} else {
@@ -99,6 +99,9 @@ return [
return $files;
},
'files' => function () {
return $this->models;
},
'data' => function () {
$data = [];
@@ -106,7 +109,7 @@ return [
// a different parent model
$dragTextAbsolute = $this->model->is($this->parent) === false;
foreach ($this->files as $file) {
foreach ($this->models as $file) {
$panel = $file->panel();
$item = [
@@ -137,7 +140,7 @@ return [
return $data;
},
'total' => function () {
return $this->files->pagination()->total();
return $this->models->pagination()->total();
},
'errors' => function () {
$errors = [];
@@ -191,13 +194,14 @@ return [
'multiple' => $multiple,
'max' => $max,
'api' => $this->parent->apiUrl(true) . '/files',
'attributes' => array_filter([
'attributes' => [
// TODO: an edge issue that needs to be solved:
// if multiple users load the same section at the same time
// and upload a file, uploaded files have the same sort number
// if multiple users load the same section
// at the same time and upload a file,
// uploaded files have the same sort number
'sort' => $this->sortable === true ? $this->total + 1 : null,
'template' => $template
])
]
];
}
],
@@ -208,7 +212,7 @@ return [
'options' => [
'accept' => $this->accept,
'apiUrl' => $this->parent->apiUrl(true),
'columns' => $this->columns,
'columns' => $this->columnsWithTypes(),
'empty' => $this->empty,
'headline' => $this->headline,
'help' => $this->help,

View File

@@ -1,5 +1,6 @@
<?php
use Kirby\Cms\ModelWithContent;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str;
@@ -28,7 +29,7 @@ return [
],
'computed' => [
'columns' => function () {
$columns = [];
$columns = [];
if ($this->layout !== 'table') {
return [];
@@ -94,7 +95,27 @@ return [
},
],
'methods' => [
'columnsValues' => function (array $item, $model) {
'columnsWithTypes' => function () {
$columns = $this->columns;
// add the type to the columns for the table layout
if ($this->layout === 'table') {
$blueprint = $this->models->first()?->blueprint();
if ($blueprint === null) {
return $columns;
}
foreach ($columns as $columnName => $column) {
if ($id = $column['id'] ?? null) {
$columns[$columnName]['type'] ??= $blueprint->field($id)['type'] ?? null;
}
}
}
return $columns;
},
'columnsValues' => function (array $item, ModelWithContent $model) {
$item['title'] = [
// override toSafeString() coming from `$item`
// because the table cells don't use v-html

View File

@@ -82,7 +82,7 @@ return [
return $parent;
},
'pages' => function () {
'models' => function () {
if ($this->query !== null) {
$pages = $this->parent->query($this->query, Pages::class) ?? new Pages([]);
} else {
@@ -156,13 +156,16 @@ return [
return $pages;
},
'pages' => function () {
return $this->models;
},
'total' => function () {
return $this->pages->pagination()->total();
return $this->models->pagination()->total();
},
'data' => function () {
$data = [];
foreach ($this->pages as $page) {
foreach ($this->models as $page) {
$panel = $page->panel();
$permissions = $page->permissions();
@@ -284,7 +287,7 @@ return [
'errors' => $this->errors,
'options' => [
'add' => $this->add,
'columns' => $this->columns,
'columns' => $this->columnsWithTypes(),
'empty' => $this->empty,
'headline' => $this->headline,
'help' => $this->help,

View File

@@ -53,6 +53,7 @@ return [
$value = $report['value'] ?? null;
$reports[] = [
'icon' => $toString($report['icon'] ?? null),
'info' => $toString(I18n::translate($info, $info)),
'label' => $toString(I18n::translate($label, $label)),
'link' => $toString(I18n::translate($link, $link)),

View File

@@ -2,6 +2,7 @@
use Kirby\Cms\Html;
use Kirby\Cms\Url;
use Kirby\Exception\NotFoundException;
use Kirby\Text\KirbyTag;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
@@ -61,7 +62,7 @@ return [
],
'html' => function (KirbyTag $tag): string {
if (!$file = $tag->file($tag->value)) {
return $tag->text;
return $tag->text ?? $tag->value;
}
// use filename if the text is empty and make sure to
@@ -197,7 +198,20 @@ return [
Uuid::is($tag->value, 'page') === true ||
Uuid::is($tag->value, 'file') === true
) {
$tag->value = Uuid::for($tag->value)->model()->url();
$tag->value = Uuid::for($tag->value)->model()?->url();
}
// if url is empty, throw exception or link to the error page
if ($tag->value === null) {
if ($tag->kirby()->option('debug', false) === true) {
if (empty($tag->text) === false) {
throw new NotFoundException('The linked page cannot be found for the link text "' . $tag->text . '"');
} else {
throw new NotFoundException('The linked page cannot be found');
}
} else {
$tag->value = Url::to($tag->kirby()->site()->errorPageId());
}
}
return Html::a($tag->value, $tag->text, [

View File

@@ -63,7 +63,7 @@
"email": "Email",
"email.placeholder": "mail@exemplo.pt",
"enter": "Enter",
"enter": "Insira",
"entries": "Registos",
"entry": "Registo",
@@ -128,7 +128,7 @@
"error.language.code": "Por favor, insira um código válido para o idioma",
"error.language.duplicate": "O idioma já existe",
"error.language.name": "Por favor, insira um nome válido para o idioma",
"error.language.notFound": "Não foi possível encontrar o idoma",
"error.language.notFound": "Não foi possível encontrar o idioma",
"error.layout.validation.block": "Há um erro no campo \"{field}\" no bloco {blockIndex} a usar o tipo de bloco \"{fieldset}\" no layout {layoutIndex}",
"error.layout.validation.settings": "Há um erro na configuração do layout {index}",
@@ -408,17 +408,17 @@
"language.variable.value": "Valor",
"languages": "Idiomas",
"languages.default": "Idioma padrão",
"languages.default": "Idioma por defeito",
"languages.empty": "Nenhum idioma ainda",
"languages.secondary": "Idiomas secundários",
"languages.secondary.empty": "Nenhum idioma secundário ainda",
"license": "Licença ",
"license.activate": "Ativar agora",
"license.activate": "Ative-a agora",
"license.activate.label": "Por favor, ative a sua licença",
"license.activate.domain": "A sua licença será ativada para <strong>{host}</strong>.",
"license.activate.local": "Está prestes a ativar a sua licença Kirby no domínio local <strong>{host}</strong>. Se este site vai ser alojado num domínio público, por favor ative-o lá. Se o domínio {host} é o o que deseja para usar a sua licença, por favor continue.",
"license.activated": "Ativado",
"license.activate.local": "Está prestes a ativar a sua licença Kirby no domínio local <strong>{host}</strong>. Se este site vai ser alojado num domínio público, por favor ative-o lá. Se o domínio {host} é o que deseja para usar a sua licença, por favor continue.",
"license.activated": "Ativada",
"license.buy": "Compre uma licença",
"license.code": "Código",
"license.code.help": "Recebeu o seu código de licença por email após a compra. Por favor, copie e cole aqui.",
@@ -459,16 +459,16 @@
"login.code.placeholder.totp": "000000",
"login.code.text.email": "Se o seu endereço de email está registado, o código solicitado foi enviado por email.",
"login.code.text.totp": "Por favor, insira o código de segurança da sua aplicação de autenticação.",
"login.email.login.body": "Olá {user.nameOrEmail},\n\nRecentemente solicitou um código de início de sessão para o Painel de {site}.\nO seguinte código de início de sessão será válido por {timeout} minutos:\n\n{code}\n\nSe não solicitou um código de início de sessão, por favor ignore este e-mail ou entre em contacto com o administrador se tiver dúvidas.\nPor motivos de segurança, por favor NÃO reencaminhe este e-mail.",
"login.email.login.body": "Olá {user.nameOrEmail},\n\nRecentemente solicitou um código de início de sessão para o painel de {site}.\nO seguinte código de início de sessão será válido por {timeout} minutos:\n\n{code}\n\nSe não solicitou um código de início de sessão, por favor ignore este e-mail ou entre em contacto com o administrador se tiver dúvidas.\nPor motivos de segurança, por favor NÃO reencaminhe este e-mail.",
"login.email.login.subject": "O seu código de início de sessão",
"login.email.password-reset.body": "Olá {user.nameOrEmail},\n\nRecentemente solicitou um código de redefinição de palavra-passe para o Painel de {site}.\nO seguinte código de redefinição de palavra-passe será válido por {timeout} minutos:\n\n{code}\n\nSe não solicitou um código de redefinição de palavra-passe, por favor ignore este e-mail ou entre em contacto com o administrador se tiver dúvidas.\nPor motivos de segurança, por favor NÃO reencaminhe este e-mail.",
"login.email.password-reset.body": "Olá {user.nameOrEmail},\n\nRecentemente solicitou um código de redefinição de palavra-passe para o painel de {site}.\nO seguinte código de redefinição de palavra-passe será válido por {timeout} minutos:\n\n{code}\n\nSe não solicitou um código de redefinição de palavra-passe, por favor ignore este e-mail ou entre em contacto com o administrador se tiver dúvidas.\nPor motivos de segurança, por favor NÃO reencaminhe este e-mail.",
"login.email.password-reset.subject": "O seu código de redefinição de palavra-passe",
"login.remember": "Manter sessão iniciada",
"login.reset": "Redefinir palavra-passe",
"login.toggleText.code.email": "Iniciar sessão com email",
"login.toggleText.code.email-password": "Iniciar sessão com palavra-passe",
"login.toggleText.password-reset.email": "Esqueceu-se da sua palavra-passe?",
"login.toggleText.password-reset.email-password": "← Voltar à página de início de sessão",
"login.toggleText.password-reset.email": "Esqueceu a sua palavra-passe?",
"login.toggleText.password-reset.email-password": "← Voltar ao início de sessão",
"login.totp.enable.option": "Configurar códigos de segurança",
"login.totp.enable.intro": "As aplicações de autenticação podem gerar códigos de segurança que são usados como um segundo fator ao iniciar a sessão na sua conta.",
"login.totp.enable.qr.label": "1. Leia este código QR",
@@ -480,7 +480,7 @@
"login.totp.enable.success": "Códigos de segurança ativados",
"login.totp.disable.option": "Desativar códigos de segurança",
"login.totp.disable.label": "Insira a sua palavra-passe para desativar códigos de segurança",
"login.totp.disable.help": "No futuro, um segundo fator diferente, como um código de início de sessão enviado por e-mail, será solicitado quando iniciar a sessão. Poderá configurar códigos únicos novamente mais tarde.",
"login.totp.disable.help": "No futuro, um segundo fator diferente, como um código de início de sessão enviado por e-mail, será solicitado quando iniciar a sessão. Poderá configurar códigos de segurança novamente mais tarde.",
"login.totp.disable.admin": "<p>Isto irá desactivar os códigos de segurança para <strong>{user}</strong>.</p> <p>No futuro, um segundo fator diferente, como um código de início de sessão enviado por e-mail, será solicitado quando eles iniciarem a sessão. {user} poderá configurar códigos de segurança novamente após o próximo início de sessão.</p>",
"login.totp.disable.success": "Códigos de segurança desativados",
@@ -684,7 +684,7 @@
"upload.errors": "Erro",
"upload.progress": "A enviar…",
"url": "Url",
"url": "URL",
"url.placeholder": "https://exemplo.pt",
"user": "Utilizador",

View File

@@ -135,7 +135,7 @@
"error.license.domain": "Domeniul pentru licență lipsește",
"error.license.email": "Te rog introdu o adresă de e-mail validă",
"error.license.format": "Please enter a valid license code",
"error.license.format": "Te rog introdu un cod de licență valid",
"error.license.verification": "Licența nu a putut fi verificată",
"error.login.totp.confirm.invalid": "Cod invalid",
@@ -425,7 +425,7 @@
"license.code.label": "Te rog introdu codul tău de licență",
"license.status.active.info": "Include noi versiuni majore până la data de {date}",
"license.status.active.label": "Licență validă",
"license.status.demo.info": "This is a demo installation",
"license.status.demo.info": "Aceasta este o instalare demo",
"license.status.demo.label": "Demo",
"license.status.inactive.info": "Reînnoiți licența pentru a actualiza la noile versiuni majore",
"license.status.inactive.label": "Fără noi versiuni majore",

File diff suppressed because one or more lines are too long

View File

@@ -77,7 +77,7 @@
<path d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM12 10.5858L14.8284 7.75736L16.2426 9.17157L13.4142 12L16.2426 14.8284L14.8284 16.2426L12 13.4142L9.17157 16.2426L7.75736 14.8284L10.5858 12L7.75736 9.17157L9.17157 7.75736L12 10.5858Z"/>
</symbol>
<symbol id="icon-cancel-small" viewBox="0 0 24 24">
<path d="M 12 10.5858 L 14.8284 7.75736 L 16.242599 9.17157 L 13.4142 12 L 16.242599 14.8284 L 14.8284 16.242599 L 12 13.4142 L 9.17157 16.242599 L 7.75736 14.8284 L 10.5858 12 L 7.75736 9.17157 L 9.17157 7.75736 Z"/>
<path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"></path>
</symbol>
<symbol id="icon-car" viewBox="0 0 24 24">
<path d="M19 20H5V21C5 21.5523 4.55228 22 4 22H3C2.44772 22 2 21.5523 2 21V11L4.4805 5.21216C4.79566 4.47679 5.51874 4 6.31879 4H17.6812C18.4813 4 19.2043 4.47679 19.5195 5.21216L22 11V21C22 21.5523 21.5523 22 21 22H20C19.4477 22 19 21.5523 19 21V20ZM20 13H4V18H20V13ZM4.17594 11H19.8241L17.6812 6H6.31879L4.17594 11ZM6.5 17C5.67157 17 5 16.3284 5 15.5C5 14.6716 5.67157 14 6.5 14C7.32843 14 8 14.6716 8 15.5C8 16.3284 7.32843 17 6.5 17ZM17.5 17C16.6716 17 16 16.3284 16 15.5C16 14.6716 16.6716 14 17.5 14C18.3284 14 19 14.6716 19 15.5C19 16.3284 18.3284 17 17.5 17Z"/>

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -20,7 +20,7 @@ window.panel.plugin = function (plugin, extensions) {
}
window.panel.plugins.components[`k-block-type-${name}`] = {
extends: "k-block-type",
extends: "k-block-type-default",
...options
};
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,95 @@
/* eslint-env node */
import path from "path";
import { defineConfig, splitVendorChunkPlugin } from "vite";
import vue from "@vitejs/plugin-vue2";
import { viteStaticCopy } from "vite-plugin-static-copy";
import externalGlobals from "rollup-plugin-external-globals";
import kirby from "./scripts/vite-kirby.mjs";
let customServer;
try {
customServer = require("./vite.config.custom.js");
} catch (err) {
customServer = {};
}
export default defineConfig(({ command }) => {
// gather plugins depending on environment
const plugins = [vue(), splitVendorChunkPlugin(), kirby()];
if (command === "build") {
plugins.push(
viteStaticCopy({
targets: [
{
src: "node_modules/vue/dist/vue.min.js",
dest: "js"
}
]
})
);
}
if (!process.env.VITEST) {
plugins.push(
// Externalize Vue so it's not loaded from node_modules
// but accessed via window.Vue
{
...externalGlobals({ vue: "Vue" }),
enforce: "post"
}
);
}
const proxy = {
target: process.env.VUE_APP_DEV_SERVER ?? "http://sandbox.test",
changeOrigin: true,
secure: false
};
return {
plugins,
define: {
// Fix vuelidate error
"process.env.BUILD": JSON.stringify("production")
},
base: "./",
build: {
minify: "terser",
cssCodeSplit: false,
rollupOptions: {
input: "./src/index.js",
output: {
entryFileNames: "js/[name].min.js",
chunkFileNames: "js/[name].min.js",
assetFileNames: "[ext]/[name].min.[ext]"
}
}
},
optimizeDeps: {
entries: "src/**/*.{js,vue}",
exclude: ["vitest", "vue"]
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src")
}
},
server: {
proxy: {
"/api": proxy,
"/env": proxy,
"/media": proxy
},
open: proxy.target + "/panel",
port: 3000,
...customServer
},
test: {
environment: "node",
include: ["**/*.test.js"],
setupFiles: ["vitest.setup.js"]
}
};
});

View File

@@ -632,10 +632,20 @@ class Api
$uploadMaxFileSize = Str::toBytes(ini_get('upload_max_filesize'));
if ($postMaxSize < $uploadMaxFileSize) {
throw new Exception(I18n::translate('upload.error.iniPostSize'));
throw new Exception(
I18n::translate(
'upload.error.iniPostSize',
'The uploaded file exceeds the post_max_size directive in php.ini'
)
);
}
throw new Exception(I18n::translate('upload.error.noFiles'));
throw new Exception(
I18n::translate(
'upload.error.noFiles',
'No files were uploaded'
)
);
}
foreach ($files as $upload) {
@@ -651,7 +661,8 @@ class Api
try {
if ($upload['error'] !== 0) {
throw new Exception(
$errorMessages[$upload['error']] ?? I18n::translate('upload.error.default')
$errorMessages[$upload['error']] ??
I18n::translate('upload.error.default', 'The file could not be uploaded')
);
}

View File

@@ -655,7 +655,7 @@ class App
* specified by the path
*
* Example:
* <?= App::image('some/page/myimage.jpg') ?>
* <?= $kirby->image('some/page/myimage.jpg') ?>
*
* @todo merge with App::file()
*/
@@ -831,9 +831,9 @@ class App
}
}
$data['kirby'] = $data['kirby'] ?? $this;
$data['site'] = $data['site'] ?? $data['kirby']->site();
$data['parent'] = $data['parent'] ?? $data['site']->page();
$data['kirby'] ??= $this;
$data['site'] ??= $data['kirby']->site();
$data['parent'] ??= $data['site']->page();
return (new KirbyTag($type, $value, $attr, $data, $this->options))->render();
}
@@ -1464,9 +1464,7 @@ class App
protected function setRoles(array $roles = null): static
{
if ($roles !== null) {
$this->roles = Roles::factory($roles, [
'kirby' => $this
]);
$this->roles = Roles::factory($roles);
}
return $this;
@@ -1480,9 +1478,7 @@ class App
protected function setSite(Site|array $site = null): static
{
if (is_array($site) === true) {
$site = new Site($site + [
'kirby' => $this
]);
$site = new Site($site);
}
$this->site = $site;
@@ -1497,7 +1493,6 @@ class App
return $this->site ??= new Site([
'errorPageId' => $this->options['error'] ?? 'error',
'homePageId' => $this->options['home'] ?? 'home',
'kirby' => $this,
'url' => $this->url('index'),
]);
}

View File

@@ -86,9 +86,7 @@ trait AppUsers
protected function setUsers(array $users = null): static
{
if ($users !== null) {
$this->users = Users::factory($users, [
'kirby' => $this
]);
$this->users = Users::factory($users);
}
return $this;
@@ -128,7 +126,6 @@ trait AppUsers
{
return $this->users ??= Users::load(
$this->root('accounts'),
['kirby' => $this]
);
}
}

View File

@@ -696,6 +696,10 @@ class Blueprint
return null;
}
if ($this->sections[$name] instanceof Section) {
return $this->sections[$name]; //@codeCoverageIgnore
}
// get all props
$props = $this->sections[$name];
@@ -703,7 +707,7 @@ class Blueprint
$props['model'] = $this->model();
// create a new section object
return new Section($props['type'], $props);
return $this->sections[$name] = new Section($props['type'], $props);
}
/**
@@ -713,7 +717,10 @@ class Blueprint
{
return A::map(
$this->sections,
fn ($section) => $this->section($section['name'])
fn ($section) => match (true) {
$section instanceof Section => $section,
default => $this->section($section['name'])
}
);
}

View File

@@ -30,6 +30,11 @@ use Kirby\Form\Field\LayoutField;
*/
class Core
{
/**
* Optional override for the auto-detected index root
*/
public static string|null $indexRoot = null;
protected array $cache = [];
protected string $root;
@@ -316,7 +321,7 @@ class Core
'i18n:translations' => fn (array $roots) => $roots['i18n'] . '/translations',
'i18n:rules' => fn (array $roots) => $roots['i18n'] . '/rules',
'index' => fn (array $roots) => dirname(__DIR__, 3),
'index' => fn (array $roots) => static::$indexRoot ?? dirname(__DIR__, 3),
'assets' => fn (array $roots) => $roots['index'] . '/assets',
'content' => fn (array $roots) => $roots['index'] . '/content',
'media' => fn (array $roots) => $roots['index'] . '/media',

View File

@@ -229,9 +229,25 @@ trait FileActions
// gather content
$content = $props['content'] ?? [];
// make sure that a UUID gets generated and
// added to content right away
if (Uuids::enabled() === true) {
// make sure that a UUID gets generated
// and added to content right away
if (
Uuids::enabled() === true &&
empty($content['uuid']) === true
) {
// sets the current uuid if it is the exact same file
if ($file->exists() === true) {
$existing = $file->parent()->file($file->filename());
if (
$file->sha1() === $upload->sha1() &&
$file->template() === $existing->template()
) {
// use existing content data if it is the exact same file
$content = $existing->content()->toArray();
}
}
$content['uuid'] ??= Uuid::generate();
}

View File

@@ -90,11 +90,9 @@ class Files extends Collection
public static function factory(array $files, Page|Site|User $parent): static
{
$collection = new static([], $parent);
$kirby = $parent->kirby();
foreach ($files as $props) {
$props['collection'] = $collection;
$props['kirby'] = $kirby;
$props['parent'] = $parent;
$file = File::factory($props);

View File

@@ -135,9 +135,14 @@ class Helpers
return true;
});
$result = $action();
restore_error_handler();
try {
$result = $action();
} finally {
// always restore the error handler, even if the
// action or the standard error handler threw an
// exception; this avoids modifying global state
restore_error_handler();
}
return $override ?? $result;
}

View File

@@ -49,9 +49,11 @@ class License
/**
* Returns the activation date if available
*/
public function activation(string|IntlDateFormatter|null $format = null): int|string|null
{
return $this->activation !== null ? Str::date(strtotime($this->activation), $format) : null;
public function activation(
string|IntlDateFormatter|null $format = null,
string|null $handler = null
): int|string|null {
return $this->activation !== null ? Str::date(strtotime($this->activation), $format, $handler) : null;
}
/**
@@ -85,9 +87,11 @@ class License
/**
* Returns the purchase date if available
*/
public function date(string|IntlDateFormatter|null $format = null): int|string|null
{
return $this->date !== null ? Str::date(strtotime($this->date), $format) : null;
public function date(
string|IntlDateFormatter|null $format = null,
string|null $handler = null
): int|string|null {
return $this->date !== null ? Str::date(strtotime($this->date), $format, $handler) : null;
}
/**
@@ -355,14 +359,16 @@ class License
/**
* Returns the renewal date
*/
public function renewal(string|IntlDateFormatter|null $format = null): int|string|null
{
public function renewal(
string|IntlDateFormatter|null $format = null,
string|null $handler = null
): int|string|null {
if ($this->activation === null) {
return null;
}
$time = strtotime('+3 years', $this->activation());
return Str::date($time, $format);
return Str::date($time, $format, $handler);
}
/**

View File

@@ -784,10 +784,17 @@ abstract class ModelWithContent implements Identifiable
]);
}
$arguments = [static::CLASS_ALIAS => $this, 'values' => $form->data(), 'strings' => $form->strings(), 'languageCode' => $languageCode];
return $this->commit('update', $arguments, function ($model, $values, $strings, $languageCode) {
return $model->save($strings, $languageCode, true);
});
return $this->commit(
'update',
[
static::CLASS_ALIAS => $this,
'values' => $form->data(),
'strings' => $form->strings(),
'languageCode' => $languageCode
],
fn ($model, $values, $strings, $languageCode) =>
$model->save($strings, $languageCode, true)
);
}
/**

View File

@@ -282,10 +282,16 @@ trait PageActions
return $this;
}
$arguments = ['page' => $this, 'status' => 'listed', 'position' => $num];
$page = $this->commit('changeStatus', $arguments, function ($page, $status, $position) {
return $page->publish()->changeNum($position);
});
$page = $this->commit(
'changeStatus',
[
'page' => $this,
'status' => 'listed',
'position' => $num
],
fn ($page, $status, $position) =>
$page->publish()->changeNum($position)
);
if ($this->blueprint()->num() === 'default') {
$page->resortSiblingsAfterListing($num);
@@ -303,10 +309,15 @@ trait PageActions
return $this;
}
$arguments = ['page' => $this, 'status' => 'unlisted', 'position' => null];
$page = $this->commit('changeStatus', $arguments, function ($page) {
return $page->publish()->changeNum(null);
});
$page = $this->commit(
'changeStatus',
[
'page' => $this,
'status' => 'unlisted',
'position' => null
],
fn ($page) => $page->publish()->changeNum(null)
);
$this->resortSiblingsAfterUnlisting();
@@ -355,7 +366,19 @@ trait PageActions
string $title,
string|null $languageCode = null
): static {
// if the `$languageCode` argument is not set and is not the default language
// the `$languageCode` argument is sent as the current language
if (
$languageCode === null &&
$language = $this->kirby()->language()
) {
if ($language->isDefault() === false) {
$languageCode = $language->code();
}
}
$arguments = ['page' => $this, 'title' => $title, 'languageCode' => $languageCode];
return $this->commit('changeTitle', $arguments, function ($page, $title, $languageCode) {
$page = $page->save(['title' => $title], $languageCode);

View File

@@ -147,7 +147,6 @@ class Pages extends Collection
): static {
$model ??= App::instance()->site();
$children = new static([], $model);
$kirby = $model->kirby();
if ($model instanceof Page) {
$parent = $model;
@@ -158,7 +157,6 @@ class Pages extends Collection
}
foreach ($pages as $props) {
$props['kirby'] = $kirby;
$props['parent'] = $parent;
$props['site'] = $site;
$props['isDraft'] = $draft ?? $props['isDraft'] ?? $props['draft'] ?? false;
@@ -445,9 +443,10 @@ class Pages extends Collection
$templates = [$templates];
}
return $this->filter(function ($page) use ($templates) {
return !in_array($page->intendedTemplate()->name(), $templates);
});
return $this->filter(
fn ($page) =>
!in_array($page->intendedTemplate()->name(), $templates)
);
}
/**
@@ -480,9 +479,10 @@ class Pages extends Collection
$templates = [$templates];
}
return $this->filter(function ($page) use ($templates) {
return in_array($page->intendedTemplate()->name(), $templates);
});
return $this->filter(
fn ($page) =>
in_array($page->intendedTemplate()->name(), $templates)
);
}
/**

View File

@@ -49,15 +49,26 @@ trait SiteActions
*/
public function changeTitle(
string $title,
string $languageCode = null
string|null $languageCode = null
): static {
$site = $this;
$title = trim($title);
$arguments = compact('site', 'title', 'languageCode');
// if the `$languageCode` argument is not set and is not the default language
// the `$languageCode` argument is sent as the current language
if (
$languageCode === null &&
$language = $this->kirby()->language()
) {
if ($language->isDefault() === false) {
$languageCode = $language->code();
}
}
return $this->commit('changeTitle', $arguments, function ($site, $title, $languageCode) {
return $site->save(['title' => $title], $languageCode);
});
$arguments = ['site' => $this, 'title' => trim($title), 'languageCode' => $languageCode];
return $this->commit(
'changeTitle',
$arguments,
fn ($site, $title, $languageCode) => $site->save(['title' => $title], $languageCode)
);
}
/**

View File

@@ -33,13 +33,17 @@ class Structure extends Items
array $items = null,
array $params = []
): static {
// Bake-in index as ID for all items
// TODO: remove when adding UUID supports to Structures
if (is_array($items) === true) {
$items = array_map(function ($item, $index) {
if (is_array($item) === true) {
// pass a clean content array without special `Item` keys
$item['content'] = $item;
// bake-in index as ID for all items
// TODO: remove when adding UUID supports to Structures
$item['id'] ??= $index;
}
return $item;
}, $items, array_keys($items));
}

View File

@@ -566,7 +566,7 @@ Database::$types['mysql'] = [
$parts[] = 'dbname=' . $params['database'];
}
$parts[] = 'charset=' . ($params['charset'] ?? 'utf8');
$parts[] = 'charset=' . ($params['charset'] ?? 'utf8mb4');
return 'mysql:' . implode(';', $parts);
}

View File

@@ -47,7 +47,8 @@ class Db
'password' => Config::get('db.password', ''),
'database' => Config::get('db.database', ''),
'prefix' => Config::get('db.prefix', ''),
'port' => Config::get('db.port', '')
'port' => Config::get('db.port', ''),
'charset' => Config::get('db.charset')
];
return static::$connection = new Database($params);

View File

@@ -169,7 +169,10 @@ class Dir
$result[] = $entry;
if ($recursive === true && is_dir($root) === true) {
$result = array_merge($result, static::index($root, true, $ignore, $entry));
$result = [
...$result,
...static::index($root, true, $ignore, $entry)
];
}
}
@@ -402,10 +405,8 @@ class Dir
$parent = dirname($dir);
if ($recursive === true) {
if (is_dir($parent) === false) {
static::make($parent, true);
}
if ($recursive === true && is_dir($parent) === false) {
static::make($parent, true);
}
if (is_writable($parent) === false) {
@@ -429,18 +430,19 @@ class Dir
* @param 'date'|'intl'|'strftime'|null $handler Custom date handler or `null`
* for the globally configured one
*/
public static function modified(string $dir, string $format = null, string|null $handler = null): int|string
{
public static function modified(
string $dir,
string $format = null,
string|null $handler = null
): int|string {
$modified = filemtime($dir);
$items = static::read($dir);
foreach ($items as $item) {
if (is_file($dir . '/' . $item) === true) {
$newModified = filemtime($dir . '/' . $item);
} else {
$newModified = static::modified($dir . '/' . $item);
}
$newModified = match (is_file($dir . '/' . $item)) {
true => filemtime($dir . '/' . $item),
false => static::modified($dir . '/' . $item)
};
$modified = ($newModified > $modified) ? $newModified : $modified;
}
@@ -595,7 +597,10 @@ class Dir
return true;
}
if (is_dir($subdir) === true && static::wasModifiedAfter($subdir, $time) === true) {
if (
is_dir($subdir) === true &&
static::wasModifiedAfter($subdir, $time) === true
) {
return true;
}
}

View File

@@ -800,11 +800,11 @@ class F
);
}
try {
return filesize($file);
} catch (Throwable) {
return 0;
if ($size = @filesize($file)) {
return $size;
}
return 0;
}
/**

View File

@@ -89,6 +89,7 @@ class Filename
'blur' => $this->blur(),
'bw' => $this->grayscale(),
'q' => $this->quality(),
'sharpen' => $this->sharpen(),
];
$array = array_filter(
@@ -244,6 +245,18 @@ class Filename
return F::safeBasename($name);
}
/**
* Normalizes the sharpen option value
*/
public function sharpen(): int|false
{
return match ($this->attributes['sharpen'] ?? false) {
false => false,
true => 50,
default => (int)$this->attributes['sharpen']
};
}
/**
* Returns the converted filename as string
*/

View File

@@ -296,7 +296,7 @@ class Form
$kirby = App::instance(null, true);
// only modify the fields if we have a valid Kirby multilang instance
if ($kirby?->multilang() === false) {
if ($kirby?->multilang() !== true) {
return $fields;
}

View File

@@ -322,10 +322,15 @@ class Environment
}
// @codeCoverageIgnoreStart
$sapi = php_sapi_name();
if ($sapi === 'cli') {
return true;
}
$term = getenv('TERM');
if (
substr(PHP_SAPI, 0, 3) === 'cgi' &&
substr($sapi, 0, 3) === 'cgi' &&
$term &&
$term !== 'unknown'
) {

View File

@@ -60,6 +60,7 @@ class Darkroom
'quality' => 90,
'scaleHeight' => null,
'scaleWidth' => null,
'sharpen' => null,
'width' => null,
];
}
@@ -93,6 +94,11 @@ class Darkroom
unset($options['bw']);
}
// normalize the sharpen option
if ($options['sharpen'] === true) {
$options['sharpen'] = 50;
}
$options['quality'] ??= $this->settings['quality'];
return $options;

View File

@@ -33,6 +33,7 @@ class GdLib extends Darkroom
$image = $this->autoOrient($image, $options);
$image = $this->blur($image, $options);
$image = $this->grayscale($image, $options);
$image = $this->sharpen($image, $options);
$image->toFile($file, $mime, $options);
@@ -116,6 +117,18 @@ class GdLib extends Darkroom
return $image->desaturate();
}
/**
* Applies sharpening if activated in the options.
*/
protected function sharpen(SimpleImage $image, array $options): SimpleImage
{
if (is_int($options['sharpen']) === false) {
return $image;
}
return $image->sharpen($options['sharpen']);
}
/**
* Returns mime type based on `format` option
*/

View File

@@ -65,14 +65,6 @@ class ImageMagick extends Darkroom
// limit to single-threading to keep CPU usage sane
$command .= ' -limit thread 1';
// add JPEG size hint to optimize CPU and memory usage
if (F::mime($file) === 'image/jpeg') {
// add hint only when downscaling
if ($options['scaleWidth'] < 1 && $options['scaleHeight'] < 1) {
$command .= ' -define ' . escapeshellarg(sprintf('jpeg:size=%dx%d', $options['width'], $options['height']));
}
}
// append input file
return $command . ' ' . escapeshellarg($file);
}
@@ -100,6 +92,19 @@ class ImageMagick extends Darkroom
return null;
}
/**
* Applies sharpening if activated in the options.
*/
protected function sharpen(string $file, array $options): string|null
{
if (is_int($options['sharpen']) === false) {
return null;
}
$amount = max(1, min(100, $options['sharpen'])) / 100;
return '-sharpen ' . escapeshellarg('0x' . $amount);
}
/**
* Applies the correct settings for interlaced JPEGs if
* activated via options
@@ -133,6 +138,7 @@ class ImageMagick extends Darkroom
$command[] = $this->resize($file, $options);
$command[] = $this->quality($file, $options);
$command[] = $this->blur($file, $options);
$command[] = $this->sharpen($file, $options);
$command[] = $this->save($file, $options);
// remove all null values and join the parts

View File

@@ -76,10 +76,11 @@ class Image extends File
}
if (in_array($this->mime(), [
'image/avif',
'image/gif',
'image/jpeg',
'image/jp2',
'image/png',
'image/gif',
'image/webp'
])) {
return $this->dimensions = Dimensions::forImage($this->root);

View File

@@ -159,7 +159,9 @@ class OptionsQuery extends OptionsProvider
}
if ($result instanceof Collection === false) {
throw new InvalidArgumentException('Invalid query result data: ' . get_class($result));
$type = is_object($result) === true ? get_class($result) : gettype($result);
throw new InvalidArgumentException('Invalid query result data: ' . $type);
}
// create options array

View File

@@ -103,6 +103,18 @@ abstract class Model
return null;
}
// switched off from blueprint,
// only if not overwritten by $settings
$blueprint = $this->model->blueprint()->image();
if ($blueprint === false) {
if (empty($settings) === true) {
return null;
}
$blueprint = null;
}
// skip image thumbnail if option
// is explicitly set to show the icon
if ($settings === 'icon') {
@@ -116,7 +128,7 @@ abstract class Model
$settings = array_merge(
$this->imageDefaults(),
$settings ?? [],
$this->model->blueprint()->image() ?? [],
$blueprint ?? [],
);
if ($image = $this->imageSource($settings['query'] ?? null)) {

View File

@@ -105,19 +105,37 @@ class PageCreateDialog
*/
public function coreFields(): array
{
$title = $this->blueprint()->create()['title']['label'] ?? 'title';
$fields = [];
return [
'title' => Field::title([
'label' => I18n::translate($title, $title),
$title = $this->blueprint()->create()['title'] ?? null;
$slug = $this->blueprint()->create()['slug'] ?? null;
if ($title === false || $slug === false) {
throw new InvalidArgumentException('Page create dialog: title and slug must not be false');
}
// title field
if ($title === null || is_array($title) === true) {
$label = $title['label'] ?? 'title';
$fields['title'] = Field::title([
...$title ?? [],
'label' => I18n::translate($label, $label),
'required' => true,
'preselect' => true
]),
'slug' => Field::slug([
]);
}
// slug field
if ($slug === null) {
$fields['slug'] = Field::slug([
'required' => true,
'sync' => 'title',
'path' => $this->parent instanceof Page ? '/' . $this->parent->id() . '/' : '/'
]),
]);
}
return [
...$fields,
'parent' => Field::hidden(),
'section' => Field::hidden(),
'template' => Field::hidden(),
@@ -171,10 +189,10 @@ class PageCreateDialog
*/
public function fields(): array
{
return array_merge(
$this->coreFields(),
$this->customFields()
);
return [
...$this->coreFields(),
...$this->customFields()
];
}
/**
@@ -188,14 +206,29 @@ class PageCreateDialog
$this->template ??= $blueprints[0]['name'];
$status = $this->blueprint()->create()['status'] ?? 'draft';
$status = $this->blueprint()->status()[$status]['label'] ?? I18n::translate('page.status.' . $status);
$status = $this->blueprint()->create()['status'] ?? 'draft';
$status = $this->blueprint()->status()[$status]['label'] ?? null;
$status ??= I18n::translate('page.status.' . $status);
$fields = $this->fields();
$visible = array_filter(
$fields,
fn ($field) => ($field['hidden'] ?? null) !== true
);
// immediately submit the dialog if there is no editable field
if (count($visible) === 0 && count($blueprints) < 2) {
$input = $this->value();
$response = $this->submit($input);
$response['redirect'] ??= $this->parent->panel()->url(true);
Panel::go($response['redirect']);
}
return [
'component' => 'k-page-create-dialog',
'props' => [
'blueprints' => $blueprints,
'fields' => $this->fields(),
'fields' => $fields,
'submitButton' => I18n::template('page.create', [
'status' => $status
]),
@@ -220,24 +253,56 @@ class PageCreateDialog
]);
}
/**
* Generates values for title and slug
* from template strings from the blueprint
*/
public function resolveFieldTemplates(array $input): array
{
$title = $this->blueprint()->create()['title'] ?? null;
$slug = $this->blueprint()->create()['slug'] ?? null;
// create temporary page object
// to resolve the template strings
$page = new Page([
'slug' => 'tmp',
'template' => $this->template,
'parent' => $this->model(),
'content' => $input
]);
if (is_string($title)) {
$input['title'] = $page->toSafeString($title);
}
if (is_string($slug)) {
$input['slug'] = $page->toSafeString($slug);
}
return $input;
}
/**
* Prepares and cleans up the input data
*/
public function sanitize(array $input): array
{
$input['slug'] ??= $this->slug ?? '';
$input['title'] ??= $this->title ?? '';
$input['slug'] ??= $this->slug ?? '';
$content = [
'title' => trim($input['title']),
];
$input = $this->resolveFieldTemplates($input);
$content = ['title' => trim($input['title'])];
foreach ($this->customFields() as $name => $field) {
$content[$name] = $input[$name] ?? null;
}
// create temporary form to sanitize the input
// and add default values
$form = Form::for($this->model(), ['values' => $content]);
return [
'content' => $content,
'content' => $form->strings(true),
'slug' => $input['slug'],
'template' => $this->template,
];
@@ -301,13 +366,22 @@ class PageCreateDialog
public function value(): array
{
return [
$value = [
'parent' => $this->parentId,
'section' => $this->sectionId,
'slug' => $this->slug ?? '',
'slug' => '',
'template' => $this->template,
'title' => $this->title ?? '',
'title' => '',
'view' => $this->viewId,
];
// add default values for custom fields
foreach ($this->customFields() as $name => $field) {
if ($default = $field['default'] ?? null) {
$value[$name] = $default;
}
}
return $value;
}
}

View File

@@ -11,7 +11,6 @@ use Kirby\Http\Cookie;
use Kirby\Http\Url;
use Kirby\Toolkit\Str;
use Kirby\Toolkit\SymmetricCrypto;
use Throwable;
/**
* @package Kirby Session
@@ -661,6 +660,7 @@ class Session
// skip if we don't have the key (only the case for moved sessions)
$hmac = Str::before($data, "\n");
$data = trim(Str::after($data, "\n"));
if (
$this->tokenKey !== null &&
hash_equals(hash_hmac('sha256', $data, $this->tokenKey), $hmac) !== true
@@ -675,16 +675,15 @@ class Session
}
// decode the serialized data
try {
$data = unserialize($data);
} catch (Throwable $e) {
$data = @unserialize($data);
if ($data === false) {
throw new LogicException([
'key' => 'session.invalid',
'data' => ['token' => $this->token()],
'fallback' => 'Session "' . $this->token() . '" is invalid',
'translate' => false,
'httpCode' => 500,
'previous' => $e
'httpCode' => 500
]);
}

View File

@@ -351,6 +351,31 @@ class A
return in_array($value, $array, $strict);
}
/**
* Join array elements as a string,
* also supporting nested arrays
*/
public static function implode(
array $array,
string $separator = ''
): string {
$result = '';
foreach ($array as $value) {
if (empty($result) === false) {
$result .= $separator;
}
if (is_array($value) === true) {
$value = static::implode($value, $separator);
}
$result .= $value;
}
return $result;
}
/**
* Checks whether an array is associative or not
*
@@ -914,9 +939,7 @@ class A
*
* // with callback
* A::update($user, [
* 'username' => function ($username) {
* return $username . ' j. simpson'
* }
* 'username' => fn ($username) => $username . ' j. simpson'
* ]);
* </code>
*/

View File

@@ -1258,74 +1258,58 @@ Collection::$filters['!='] = function ($collection, $field, $test, $split = fals
* In Filter
*/
Collection::$filters['in'] = [
'validator' => function ($value, $test) {
return in_array($value, $test) === true;
},
'strict' => false
'validator' => fn ($value, $test) => in_array($value, $test) === true,
'strict' => false
];
/**
* Not In Filter
*/
Collection::$filters['not in'] = [
'validator' => function ($value, $test) {
return in_array($value, $test) === false;
},
'validator' => fn ($value, $test) => in_array($value, $test) === false
];
/**
* Contains Filter
*/
Collection::$filters['*='] = [
'validator' => function ($value, $test) {
return strpos($value, $test) !== false;
},
'strict' => false
'validator' => fn ($value, $test) => strpos($value, $test) !== false,
'strict' => false
];
/**
* Not Contains Filter
*/
Collection::$filters['!*='] = [
'validator' => function ($value, $test) {
return strpos($value, $test) === false;
},
'validator' => fn ($value, $test) => strpos($value, $test) === false
];
/**
* More Filter
*/
Collection::$filters['>'] = [
'validator' => function ($value, $test) {
return $value > $test;
}
'validator' => fn ($value, $test) => $value > $test
];
/**
* Min Filter
*/
Collection::$filters['>='] = [
'validator' => function ($value, $test) {
return $value >= $test;
}
'validator' => fn ($value, $test) => $value >= $test
];
/**
* Less Filter
*/
Collection::$filters['<'] = [
'validator' => function ($value, $test) {
return $value < $test;
}
'validator' => fn ($value, $test) => $value < $test
];
/**
* Max Filter
*/
Collection::$filters['<='] = [
'validator' => function ($value, $test) {
return $value <= $test;
}
'validator' => fn ($value, $test) => $value <= $test
];
/**
@@ -1340,9 +1324,7 @@ Collection::$filters['$='] = [
* Not Ends With Filter
*/
Collection::$filters['!$='] = [
'validator' => function ($value, $test) {
return V::endsWith($value, $test) === false;
}
'validator' => fn ($value, $test) => V::endsWith($value, $test) === false
];
/**
@@ -1357,19 +1339,15 @@ Collection::$filters['^='] = [
* Not Starts With Filter
*/
Collection::$filters['!^='] = [
'validator' => function ($value, $test) {
return V::startsWith($value, $test) === false;
}
'validator' => fn ($value, $test) => V::startsWith($value, $test) === false
];
/**
* Between Filter
*/
Collection::$filters['between'] = Collection::$filters['..'] = [
'validator' => function ($value, $test) {
return V::between($value, ...$test) === true;
},
'strict' => false
'validator' => fn ($value, $test) => V::between($value, ...$test) === true,
'strict' => false
];
/**
@@ -1384,9 +1362,7 @@ Collection::$filters['*'] = [
* Not Match Filter
*/
Collection::$filters['!*'] = [
'validator' => function ($value, $test) {
return V::match($value, $test) === false;
}
'validator' => fn ($value, $test) => V::match($value, $test) === false
];
/**
@@ -1421,63 +1397,49 @@ Collection::$filters['minwords'] = [
* Date Equals Filter
*/
Collection::$filters['date =='] = [
'validator' => function ($value, $test) {
return V::date($value, '==', $test);
}
'validator' => fn ($value, $test) => V::date($value, '==', $test)
];
/**
* Date Not Equals Filter
*/
Collection::$filters['date !='] = [
'validator' => function ($value, $test) {
return V::date($value, '!=', $test);
}
'validator' => fn ($value, $test) => V::date($value, '!=', $test)
];
/**
* Date More Filter
*/
Collection::$filters['date >'] = [
'validator' => function ($value, $test) {
return V::date($value, '>', $test);
}
'validator' => fn ($value, $test) => V::date($value, '>', $test)
];
/**
* Date Min Filter
*/
Collection::$filters['date >='] = [
'validator' => function ($value, $test) {
return V::date($value, '>=', $test);
}
'validator' => fn ($value, $test) => V::date($value, '>=', $test)
];
/**
* Date Less Filter
*/
Collection::$filters['date <'] = [
'validator' => function ($value, $test) {
return V::date($value, '<', $test);
}
'validator' => fn ($value, $test) => V::date($value, '<', $test)
];
/**
* Date Max Filter
*/
Collection::$filters['date <='] = [
'validator' => function ($value, $test) {
return V::date($value, '<=', $test);
}
'validator' => fn ($value, $test) => V::date($value, '<=', $test)
];
/**
* Date Between Filter
*/
Collection::$filters['date between'] = Collection::$filters['date ..'] = [
'validator' => function ($value, $test) {
return
'validator' => fn ($value, $test) =>
V::date($value, '>=', $test[0]) &&
V::date($value, '<=', $test[1]);
}
V::date($value, '<=', $test[1])
];

View File

@@ -222,7 +222,7 @@ class Date extends DateTime
*/
public function microsecond(): int
{
return $this->format('u');
return (int)$this->format('u');
}
/**
@@ -230,7 +230,7 @@ class Date extends DateTime
*/
public function millisecond(): int
{
return $this->format('v');
return (int)$this->format('v');
}
/**

View File

@@ -698,7 +698,7 @@ class Str
string $string = null,
string $needle,
bool $caseInsensitive = false
): int|bool {
): int|false {
if ($needle === '') {
throw new InvalidArgumentException('The needle must not be empty');
}

View File

@@ -134,16 +134,13 @@ class V
$value = $params[$index] ?? null;
if (is_array($value) === true) {
try {
foreach ($value as $key => $item) {
if (is_array($item) === true) {
$value[$key] = implode('|', $item);
}
foreach ($value as $key => $item) {
if (is_array($item) === true) {
$value[$key] = A::implode($item, '|');
}
$value = implode(', ', $value);
} catch (Throwable) {
$value = '-';
}
$value = implode(', ', $value);
}
$arguments[$parameter->getName()] = $value;

View File

@@ -1,8 +1,8 @@
<?php return array(
'root' => array(
'name' => 'getkirby/cms',
'pretty_version' => '4.0.3',
'version' => '4.0.3.0',
'pretty_version' => '4.1.0',
'version' => '4.1.0.0',
'reference' => NULL,
'type' => 'kirby-cms',
'install_path' => __DIR__ . '/../../',
@@ -47,8 +47,8 @@
'dev_requirement' => false,
),
'getkirby/cms' => array(
'pretty_version' => '4.0.3',
'version' => '4.0.3.0',
'pretty_version' => '4.1.0',
'version' => '4.1.0.0',
'reference' => NULL,
'type' => 'kirby-cms',
'install_path' => __DIR__ . '/../../',