Upgrade to 3.9.0

This commit is contained in:
Bastian Allgeier
2023-01-17 14:50:16 +01:00
parent 0ebe0c7b16
commit 6e5c9d1f48
132 changed files with 1664 additions and 1254 deletions

View File

@@ -20,7 +20,7 @@ We will review all pull requests (PRs) to `develop` and merge them if accepted,
### Report a bug ### Report a bug
When you find a bug, the first step to fixing it is to help us understand and reproduce the bug as best as possible. When you create a bug report, please include as many details as possible. Fill out [the template](ISSUE_TEMPLATE/bug_report.md) because the requested information helps us resolve issues so much faster. When you find a bug, the first step to fixing it is to help us understand and reproduce the bug as best as possible. When you create a bug report, please include as many details as possible. Fill out [the template](https://github.com/getkirby/kirby/issues/new?template=bug_report.md) because the requested information helps us resolve issues so much faster.
### Bug fixes ### Bug fixes

View File

@@ -6,7 +6,7 @@
*/ */
if ( if (
version_compare(PHP_VERSION, '8.0.0', '>=') === false || version_compare(PHP_VERSION, '8.0.0', '>=') === false ||
version_compare(PHP_VERSION, '8.2.0', '<') === false version_compare(PHP_VERSION, '8.3.0', '<') === false
) { ) {
die(include __DIR__ . '/views/php.php'); die(include __DIR__ . '/views/php.php');
} }

View File

@@ -3,7 +3,7 @@
"description": "The Kirby 3 core", "description": "The Kirby 3 core",
"license": "proprietary", "license": "proprietary",
"type": "kirby-cms", "type": "kirby-cms",
"version": "3.8.4", "version": "3.9.0",
"keywords": [ "keywords": [
"kirby", "kirby",
"cms", "cms",
@@ -24,7 +24,7 @@
"source": "https://github.com/getkirby/kirby" "source": "https://github.com/getkirby/kirby"
}, },
"require": { "require": {
"php": ">=8.0.0 <8.2.0", "php": ">=8.0.0 <8.3.0",
"ext-SimpleXML": "*", "ext-SimpleXML": "*",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",
@@ -36,15 +36,15 @@
"ext-libxml": "*", "ext-libxml": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-openssl": "*", "ext-openssl": "*",
"claviska/simpleimage": "3.7.0", "claviska/simpleimage": "3.7.2",
"composer/semver": "3.3.2", "composer/semver": "3.3.2",
"filp/whoops": "2.14.6", "filp/whoops": "2.14.6",
"getkirby/composer-installer": "^1.2.1", "getkirby/composer-installer": "^1.2.1",
"laminas/laminas-escaper": "2.12.0", "laminas/laminas-escaper": "2.12.0",
"michelf/php-smartypants": "1.8.1", "michelf/php-smartypants": "1.8.1",
"phpmailer/phpmailer": "6.6.5", "phpmailer/phpmailer": "6.7.1",
"symfony/polyfill-intl-idn": "1.26.0", "symfony/polyfill-intl-idn": "1.27.0",
"symfony/polyfill-mbstring": "1.26.0" "symfony/polyfill-mbstring": "1.27.0"
}, },
"replace": { "replace": {
"symfony/polyfill-php72": "*" "symfony/polyfill-php72": "*"
@@ -105,7 +105,8 @@
"@test" "@test"
], ],
"fix": "php-cs-fixer fix", "fix": "php-cs-fixer fix",
"test": "phpunit --stderr --coverage-html=tests/coverage", "test": "phpunit --stderr",
"test:coverage": "phpunit --stderr --coverage-html=tests/coverage",
"zip": "composer archive --format=zip --file=dist" "zip": "composer archive --format=zip --file=dist"
} }
} }

66
kirby/composer.lock generated
View File

@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c18d76f29686da553201003f5cefe0bf", "content-hash": "65b578807e38bf1bf4af1ba64445d9f9",
"packages": [ "packages": [
{ {
"name": "claviska/simpleimage", "name": "claviska/simpleimage",
"version": "3.7.0", "version": "3.7.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/claviska/SimpleImage.git", "url": "https://github.com/claviska/SimpleImage.git",
"reference": "abd15ced313c7b8041d7d73d8d2398b4f2510cf1" "reference": "82dbef988e356baa5d73993a1351bcb6c0959269"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/claviska/SimpleImage/zipball/abd15ced313c7b8041d7d73d8d2398b4f2510cf1", "url": "https://api.github.com/repos/claviska/SimpleImage/zipball/82dbef988e356baa5d73993a1351bcb6c0959269",
"reference": "abd15ced313c7b8041d7d73d8d2398b4f2510cf1", "reference": "82dbef988e356baa5d73993a1351bcb6c0959269",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -45,7 +45,7 @@
"description": "A PHP class that makes working with images as simple as possible.", "description": "A PHP class that makes working with images as simple as possible.",
"support": { "support": {
"issues": "https://github.com/claviska/SimpleImage/issues", "issues": "https://github.com/claviska/SimpleImage/issues",
"source": "https://github.com/claviska/SimpleImage/tree/3.7.0" "source": "https://github.com/claviska/SimpleImage/tree/3.7.2"
}, },
"funding": [ "funding": [
{ {
@@ -53,7 +53,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-07-05T13:18:44+00:00" "time": "2022-12-12T14:31:53+00:00"
}, },
{ {
"name": "composer/semver", "name": "composer/semver",
@@ -430,16 +430,16 @@
}, },
{ {
"name": "phpmailer/phpmailer", "name": "phpmailer/phpmailer",
"version": "v6.6.5", "version": "v6.7.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git", "url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "8b6386d7417526d1ea4da9edb70b8352f7543627" "reference": "49cd7ea3d2563f028d7811f06864a53b1f15ff55"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/8b6386d7417526d1ea4da9edb70b8352f7543627", "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/49cd7ea3d2563f028d7811f06864a53b1f15ff55",
"reference": "8b6386d7417526d1ea4da9edb70b8352f7543627", "reference": "49cd7ea3d2563f028d7811f06864a53b1f15ff55",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -449,17 +449,19 @@
"php": ">=5.5.0" "php": ">=5.5.0"
}, },
"require-dev": { "require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2",
"doctrine/annotations": "^1.2", "doctrine/annotations": "^1.2.6 || ^1.13.3",
"php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-console-highlighter": "^1.0.0",
"php-parallel-lint/php-parallel-lint": "^1.3.2", "php-parallel-lint/php-parallel-lint": "^1.3.2",
"phpcompatibility/php-compatibility": "^9.3.5", "phpcompatibility/php-compatibility": "^9.3.5",
"roave/security-advisories": "dev-latest", "roave/security-advisories": "dev-latest",
"squizlabs/php_codesniffer": "^3.6.2", "squizlabs/php_codesniffer": "^3.7.1",
"yoast/phpunit-polyfills": "^1.0.0" "yoast/phpunit-polyfills": "^1.0.4"
}, },
"suggest": { "suggest": {
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication", "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging", "psr/log": "For optional PSR-3 debug logging",
@@ -496,7 +498,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP", "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": { "support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues", "issues": "https://github.com/PHPMailer/PHPMailer/issues",
"source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.5" "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.7.1"
}, },
"funding": [ "funding": [
{ {
@@ -504,7 +506,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2022-10-07T12:23:10+00:00" "time": "2022-12-08T13:30:06+00:00"
}, },
{ {
"name": "psr/log", "name": "psr/log",
@@ -558,16 +560,16 @@
}, },
{ {
"name": "symfony/polyfill-intl-idn", "name": "symfony/polyfill-intl-idn",
"version": "v1.26.0", "version": "v1.27.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git", "url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" "reference": "639084e360537a19f9ee352433b84ce831f3d2da"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da",
"reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", "reference": "639084e360537a19f9ee352433b84ce831f3d2da",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -581,7 +583,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.26-dev" "dev-main": "1.27-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@@ -625,7 +627,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0"
}, },
"funding": [ "funding": [
{ {
@@ -641,7 +643,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-05-24T11:49:31+00:00" "time": "2022-11-03T14:55:06+00:00"
}, },
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
@@ -729,16 +731,16 @@
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.26.0", "version": "v1.27.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -753,7 +755,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "1.26-dev" "dev-main": "1.27-dev"
}, },
"thanks": { "thanks": {
"name": "symfony/polyfill", "name": "symfony/polyfill",
@@ -792,7 +794,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
}, },
"funding": [ "funding": [
{ {
@@ -808,7 +810,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-05-24T11:49:31+00:00" "time": "2022-11-03T14:55:06+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],
@@ -818,7 +820,7 @@
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=8.0.0 <8.2.0", "php": ">=8.0.0 <8.3.0",
"ext-simplexml": "*", "ext-simplexml": "*",
"ext-ctype": "*", "ext-ctype": "*",
"ext-curl": "*", "ext-curl": "*",

View File

@@ -53,6 +53,10 @@ return [
// panel classes // panel classes
'panel' => 'Kirby\Panel\Panel', 'panel' => 'Kirby\Panel\Panel',
// template classes
'snippet' => 'Kirby\Template\Snippet',
'slot' => 'Kirby\Template\Slot',
// toolkit classes // toolkit classes
'a' => 'Kirby\Toolkit\A', 'a' => 'Kirby\Toolkit\A',
'c' => 'Kirby\Toolkit\Config', 'c' => 'Kirby\Toolkit\Config',
@@ -73,6 +77,7 @@ return [
'kirby\cms\form' => 'Kirby\Form\Form', 'kirby\cms\form' => 'Kirby\Form\Form',
'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag', 'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag',
'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags', 'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags',
'kirby\cms\template' => 'Kirby\Template\Template',
'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir', 'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir',
'kirby\toolkit\f' => 'Kirby\Filesystem\F', 'kirby\toolkit\f' => 'Kirby\Filesystem\F',
'kirby\toolkit\file' => 'Kirby\Filesystem\File', 'kirby\toolkit\file' => 'Kirby\Filesystem\File',

View File

@@ -35,7 +35,8 @@ return [
$code = $this->user()?->language() ?? $code = $this->user()?->language() ??
$this->kirby()->panelLanguage(); $this->kirby()->panelLanguage();
return $this->kirby()->translation($code) ?? return
$this->kirby()->translation($code) ??
$this->kirby()->translation('en'); $this->kirby()->translation('en');
}, },
'kirbytext' => fn () => $this->kirby()->option('panel.kirbytext') ?? true, 'kirbytext' => fn () => $this->kirby()->option('panel.kirbytext') ?? true,

View File

@@ -38,14 +38,17 @@ return [
// move_uploaded_file() not working with unit test // move_uploaded_file() not working with unit test
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
return $this->upload(function ($source, $filename) use ($path) { return $this->upload(function ($source, $filename) use ($path) {
return $this->parent($path)->createFile([ $props = [
'content' => [ 'content' => [
'sort' => $this->requestBody('sort') 'sort' => $this->requestBody('sort')
], ],
'source' => $source, 'source' => $source,
'template' => $this->requestBody('template'), 'template' => $this->requestBody('template'),
'filename' => $filename 'filename' => $filename
]); ];
// move the source file from the temp dir
return $this->parent($path)->createFile($props, true);
}); });
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
} }
@@ -95,8 +98,9 @@ return [
'pattern' => $pattern . '/files/(:any)', 'pattern' => $pattern . '/files/(:any)',
'method' => 'POST', 'method' => 'POST',
'action' => function (string $path, string $filename) { 'action' => function (string $path, string $filename) {
// move the source file from the temp dir
return $this->upload( return $this->upload(
fn ($source) => $this->file($path, $filename)->replace($source) fn ($source) => $this->file($path, $filename)->replace($source, true)
); );
} }
], ],

View File

@@ -4,6 +4,9 @@
/** /**
* Content Lock Routes * Content Lock Routes
*/ */
use Kirby\Exception\NotFoundException;
return [ return [
[ [
'pattern' => '(:all)/lock', 'pattern' => '(:all)/lock',
@@ -25,7 +28,11 @@ return [
'pattern' => '(:all)/lock', 'pattern' => '(:all)/lock',
'method' => 'DELETE', 'method' => 'DELETE',
'action' => function (string $path) { 'action' => function (string $path) {
try {
return $this->parent($path)->lock()?->remove(); return $this->parent($path)->lock()?->remove();
} catch (NotFoundException) {
return true;
}
} }
], ],
[ [
@@ -39,7 +46,11 @@ return [
'pattern' => '(:all)/unlock', 'pattern' => '(:all)/unlock',
'method' => 'DELETE', 'method' => 'DELETE',
'action' => function (string $path) { 'action' => function (string $path) {
try {
return $this->parent($path)->lock()?->resolve(); return $this->parent($path)->lock()?->resolve();
} catch (NotFoundException) {
return true;
}
} }
], ],
]; ];

View File

@@ -82,11 +82,16 @@ return [
$this->user($id)->avatar()?->delete(); $this->user($id)->avatar()?->delete();
return $this->upload( return $this->upload(
fn ($source, $filename) => $this->user($id)->createFile([ function ($source, $filename) {
$props = [
'filename' => 'profile.' . F::extension($filename), 'filename' => 'profile.' . F::extension($filename),
'template' => 'avatar', 'template' => 'avatar',
'source' => $source 'source' => $source
]), ];
// move the source file from the temp dir
return $this->user($id)->createFile($props, true);
},
single: true single: true
); );
} }

View File

@@ -44,10 +44,17 @@ return [
// license registration // license registration
'registration' => [ 'registration' => [
'load' => function () { 'load' => function () {
$system = App::instance()->system();
return [ return [
'component' => 'k-form-dialog', 'component' => 'k-form-dialog',
'props' => [ 'props' => [
'fields' => [ 'fields' => [
'domain' => [
'type' => 'info',
'theme' => $system->isLocal() ? 'notice' : 'info',
'text' => I18n::template('license.register.' . ($system->isLocal() ? 'local' : 'domain'), ['host' => $system->indexUrl()])
],
'license' => [ 'license' => [
'label' => I18n::translate('license.register.label'), 'label' => I18n::translate('license.register.label'),
'type' => 'text', 'type' => 'text',
@@ -56,9 +63,7 @@ return [
'placeholder' => 'K3-', 'placeholder' => 'K3-',
'help' => I18n::translate('license.register.help') 'help' => I18n::translate('license.register.help')
], ],
'email' => Field::email([ 'email' => Field::email(['required' => true])
'required' => true
])
], ],
'submitButton' => I18n::translate('license.register'), 'submitButton' => I18n::translate('license.register'),
'value' => [ 'value' => [

View File

@@ -5,7 +5,6 @@ use Kirby\Cms\Collection;
use Kirby\Cms\File; use Kirby\Cms\File;
use Kirby\Cms\FileVersion; use Kirby\Cms\FileVersion;
use Kirby\Cms\Page; use Kirby\Cms\Page;
use Kirby\Cms\Template;
use Kirby\Cms\User; use Kirby\Cms\User;
use Kirby\Data\Data; use Kirby\Data\Data;
use Kirby\Email\PHPMailer as Emailer; use Kirby\Email\PHPMailer as Emailer;
@@ -14,11 +13,12 @@ use Kirby\Filesystem\Filename;
use Kirby\Http\Uri; use Kirby\Http\Uri;
use Kirby\Http\Url; use Kirby\Http\Url;
use Kirby\Image\Darkroom; use Kirby\Image\Darkroom;
use Kirby\Template\Snippet;
use Kirby\Template\Template;
use Kirby\Text\Markdown; use Kirby\Text\Markdown;
use Kirby\Text\SmartyPants; use Kirby\Text\SmartyPants;
use Kirby\Toolkit\A; use Kirby\Toolkit\A;
use Kirby\Toolkit\Str; use Kirby\Toolkit\Str;
use Kirby\Toolkit\Tpl as Snippet;
return [ return [
@@ -142,6 +142,7 @@ return [
* @return \Kirby\Cms\Collection|bool * @return \Kirby\Cms\Collection|bool
*/ */
'search' => function (App $kirby, Collection $collection, string $query = null, $params = []) { 'search' => function (App $kirby, Collection $collection, string $query = null, $params = []) {
// empty search query
if (empty(trim($query ?? '')) === true) { if (empty(trim($query ?? '')) === true) {
return $collection->limit(0); return $collection->limit(0);
} }
@@ -159,26 +160,29 @@ return [
$options = array_merge($defaults, $params); $options = array_merge($defaults, $params);
$collection = clone $collection; $collection = clone $collection;
$searchWords = preg_replace('/(\s)/u', ',', $query);
$searchWords = Str::split($searchWords, ',', $options['minlength']); $words = preg_replace('/(\s)/u', ',', $query);
$lowerQuery = Str::lower($query); $words = Str::split($words, ',', $options['minlength']);
$exactQuery = $options['words'] ? '(\b' . preg_quote($query) . '\b)' : preg_quote($query); $exact = $options['words'] ? '(\b' . preg_quote($query) . '\b)' : preg_quote($query);
$query = Str::lower($query);
if (empty($options['stopwords']) === false) { if (empty($options['stopwords']) === false) {
$searchWords = array_diff($searchWords, $options['stopwords']); $words = array_diff($words, $options['stopwords']);
} }
$searchWords = array_map(function ($value) use ($options) { $words = A::map(
return $options['words'] ? '\b' . preg_quote($value) . '\b' : preg_quote($value); $words,
}, $searchWords); fn ($value) => $options['words'] ? '\b' . preg_quote($value) . '\b' : preg_quote($value)
);
// returns an empty collection if there is no search word // returns an empty collection if there is no search word
if (empty($searchWords) === true) { if (empty($words) === true) {
return $collection->limit(0); return $collection->limit(0);
} }
$preg = '!(' . implode('|', $searchWords) . ')!i'; $preg = '!(' . implode('|', $words) . ')!i';
$results = $collection->filter(function ($item) use ($query, $preg, $options, $lowerQuery, $exactQuery) { $scores = [];
$results = $collection->filter(function ($item) use ($query, $exact, $preg, $options, &$scores) {
$data = $item->content()->toArray(); $data = $item->content()->toArray();
$keys = array_keys($data); $keys = array_keys($data);
$keys[] = 'id'; $keys[] = 'id';
@@ -200,8 +204,10 @@ return [
$keys = array_intersect($keys, $fields); $keys = array_intersect($keys, $fields);
} }
$item->searchHits = 0; $scoring = [
$item->searchScore = 0; 'hits' => 0,
'score' => 0
];
foreach ($keys as $key) { foreach ($keys as $key) {
$score = $options['score'][$key] ?? 1; $score = $options['score'][$key] ?? 1;
@@ -210,32 +216,39 @@ return [
$lowerValue = Str::lower($value); $lowerValue = Str::lower($value);
// check for exact matches // check for exact matches
if ($lowerQuery == $lowerValue) { if ($query == $lowerValue) {
$item->searchScore += 16 * $score; $scoring['score'] += 16 * $score;
$item->searchHits += 1; $scoring['hits'] += 1;
// check for exact beginning matches // check for exact beginning matches
} elseif ($options['words'] === false && Str::startsWith($lowerValue, $lowerQuery) === true) { } elseif (
$item->searchScore += 8 * $score; $options['words'] === false &&
$item->searchHits += 1; Str::startsWith($lowerValue, $query) === true
) {
$scoring['score'] += 8 * $score;
$scoring['hits'] += 1;
// check for exact query matches // check for exact query matches
} elseif ($matches = preg_match_all('!' . $exactQuery . '!i', $value, $r)) { } elseif ($matches = preg_match_all('!' . $exact . '!i', $value, $r)) {
$item->searchScore += 2 * $score; $scoring['score'] += 2 * $score;
$item->searchHits += $matches; $scoring['hits'] += $matches;
} }
// check for any match // check for any match
if ($matches = preg_match_all($preg, $value, $r)) { if ($matches = preg_match_all($preg, $value, $r)) {
$item->searchHits += $matches; $scoring['score'] += $matches * $score;
$item->searchScore += $matches * $score; $scoring['hits'] += $matches;
} }
} }
return $item->searchHits > 0; $scores[$item->id()] = $scoring;
return $scoring['hits'] > 0;
}); });
return $results->sort('searchScore', 'desc'); return $results->sort(
fn ($item) => $scores[$item->id()]['score'],
'desc'
);
}, },
/** /**
@@ -267,23 +280,8 @@ return [
* @param string|array $name Snippet name * @param string|array $name Snippet name
* @param array $data Data array for the snippet * @param array $data Data array for the snippet
*/ */
'snippet' => function (App $kirby, $name, array $data = []): string { 'snippet' => function (App $kirby, string|array $name, array $data = [], bool $slots = false): Snippet|string {
$snippets = A::wrap($name); return Snippet::factory($name, $data, $slots);
foreach ($snippets as $name) {
$name = (string)$name;
$file = $kirby->root('snippets') . '/' . $name . '.php';
if (file_exists($file) === false) {
$file = $kirby->extensions('snippets')[$name] ?? null;
}
if ($file) {
break;
}
}
return Snippet::load($file, $data);
}, },
/** /**
@@ -293,7 +291,7 @@ return [
* @param string $name Template name * @param string $name Template name
* @param string $type Extension type * @param string $type Extension type
* @param string $defaultType Default extension type * @param string $defaultType Default extension type
* @return \Kirby\Cms\Template * @return \Kirby\Template\Template
*/ */
'template' => function (App $kirby, string $name, string $type = 'html', string $defaultType = 'html') { 'template' => function (App $kirby, string $name, string $type = 'html', string $defaultType = 'html') {
return new Template($name, $type, $defaultType); return new Template($name, $type, $defaultType);

View File

@@ -56,11 +56,14 @@ return [
} }
return $api->upload(function ($source, $filename) use ($parent, $params, $map) { return $api->upload(function ($source, $filename) use ($parent, $params, $map) {
$file = $parent->createFile([ $props = [
'source' => $source, 'source' => $source,
'template' => $params['template'] ?? null, 'template' => $params['template'] ?? null,
'filename' => $filename, 'filename' => $filename,
]); ];
// move the source file from the temp dir
$file = $parent->createFile($props, true);
if ($file instanceof File === false) { if ($file instanceof File === false) {
throw new Exception('The file could not be uploaded'); throw new Exception('The file could not be uploaded');

View File

@@ -1,5 +1,7 @@
<?php <?php
use Kirby\Field\FieldOptions;
return [ return [
'extends' => 'radio', 'extends' => 'radio',
'props' => [ 'props' => [
@@ -20,5 +22,16 @@ return [
'placeholder' => function (string $placeholder = '—') { 'placeholder' => function (string $placeholder = '—') {
return $placeholder; return $placeholder;
}, },
],
'methods' => [
'getOptions' => function () {
$props = FieldOptions::polyfill($this->props);
// disable safe mode as the select field does not
// render HTML for the option text
$options = FieldOptions::factory($props['options'], false);
return $options->render($this->model());
}
] ]
]; ];

View File

@@ -10,7 +10,7 @@ return [
* The field value will be converted with the selected converter before the value gets saved. Available converters: `lower`, `upper`, `ucfirst`, `slug` * The field value will be converted with the selected converter before the value gets saved. Available converters: `lower`, `upper`, `ucfirst`, `slug`
*/ */
'converter' => function ($value = null) { 'converter' => function ($value = null) {
if ($value !== null && in_array($value, array_keys($this->converters())) === false) { if ($value !== null && array_key_exists($value, $this->converters()) === false) {
throw new InvalidArgumentException([ throw new InvalidArgumentException([
'key' => 'field.converter.invalid', 'key' => 'field.converter.invalid',
'data' => ['converter' => $value] 'data' => ['converter' => $value]

View File

@@ -8,6 +8,8 @@ use Kirby\Cms\Url;
use Kirby\Filesystem\Asset; use Kirby\Filesystem\Asset;
use Kirby\Filesystem\F; use Kirby\Filesystem\F;
use Kirby\Http\Router; use Kirby\Http\Router;
use Kirby\Template\Slot;
use Kirby\Template\Snippet;
use Kirby\Toolkit\Date; use Kirby\Toolkit\Date;
use Kirby\Toolkit\I18n; use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Str; use Kirby\Toolkit\Str;
@@ -131,6 +133,26 @@ if (Helpers::hasOverride('e') === false) { // @codeCoverageIgnore
} }
} }
if (Helpers::hasOverride('endslot') === false) { // @codeCoverageIgnore
/**
* Ends the last started template slot
*/
function endslot(): void
{
Slot::end();
}
}
if (Helpers::hasOverride('endsnippet') === false) { // @codeCoverageIgnore
/**
* Renders the currently active snippet with slots
*/
function endsnippet(): void
{
Snippet::end();
}
}
if (Helpers::hasOverride('esc') === false) { // @codeCoverageIgnore if (Helpers::hasOverride('esc') === false) { // @codeCoverageIgnore
/** /**
* Escape context specific output * Escape context specific output
@@ -538,6 +560,16 @@ if (Helpers::hasOverride('size') === false) { // @codeCoverageIgnore
} }
} }
if (Helpers::hasOverride('slot') === false) { // @codeCoverageIgnore
/**
* Starts a new template slot
*/
function slot(string $name = 'default'): void
{
Slot::begin($name);
}
}
if (Helpers::hasOverride('smartypants') === false) { // @codeCoverageIgnore if (Helpers::hasOverride('smartypants') === false) { // @codeCoverageIgnore
/** /**
* Enhances the given string with * Enhances the given string with
@@ -555,15 +587,14 @@ if (Helpers::hasOverride('smartypants') === false) { // @codeCoverageIgnore
if (Helpers::hasOverride('snippet') === false) { // @codeCoverageIgnore if (Helpers::hasOverride('snippet') === false) { // @codeCoverageIgnore
/** /**
* Embeds a snippet from the snippet folder * Embeds a snippet from the snippet folder
*
* @param string|array $name
* @param array|object $data
* @param bool $return
* @return string|null
*/ */
function snippet($name, $data = [], bool $return = false): string|null function snippet(
{ $name,
return App::instance()->snippet($name, $data, $return); $data = [],
bool $return = false,
bool $slots = false
): Snippet|string|null {
return App::instance()->snippet($name, $data, $return, $slots);
} }
} }

View File

@@ -326,7 +326,7 @@ return function (App $app) {
* Returns the number of words in the text * Returns the number of words in the text
*/ */
'words' => function (Field $field) { 'words' => function (Field $field) {
return str_word_count(strip_tags($field->value)); return str_word_count(strip_tags($field->value ?? ''));
}, },
// manipulators // manipulators

View File

@@ -571,7 +571,8 @@ class ParsedownExtra extends Parsedown
$DOMDocument = new DOMDocument(); $DOMDocument = new DOMDocument();
# http://stackoverflow.com/q/11309194/200145 # http://stackoverflow.com/q/11309194/200145
$elementMarkup = mb_convert_encoding($elementMarkup, 'HTML-ENTITIES', 'UTF-8'); $elementMarkup = htmlentities($elementMarkup);
$elementMarkup = htmlspecialchars_decode($elementMarkup);
# Ensure that saveHTML() is not remove new line characters. New lines will be split by this character. # Ensure that saveHTML() is not remove new line characters. New lines will be split by this character.
$DOMDocument->formatOutput = true; $DOMDocument->formatOutput = true;

View File

@@ -75,6 +75,7 @@
"error.email.preset.notFound": "The email preset \"{name}\" cannot be found", "error.email.preset.notFound": "The email preset \"{name}\" cannot be found",
"error.field.converter.invalid": "Invalid converter \"{converter}\"", "error.field.converter.invalid": "Invalid converter \"{converter}\"",
"error.field.type.missing": "Field \"{ name }\": The field type \"{ type }\" does not exist",
"error.file.changeName.empty": "The name must not be empty", "error.file.changeName.empty": "The name must not be empty",
"error.file.changeName.permission": "You are not allowed to change the name of \"{filename}\"", "error.file.changeName.permission": "You are not allowed to change the name of \"{filename}\"",
@@ -289,6 +290,7 @@
"field.pages.empty": "No pages selected yet", "field.pages.empty": "No pages selected yet",
"field.structure.delete.confirm": "Do you really want to delete this row?", "field.structure.delete.confirm": "Do you really want to delete this row?",
"field.structure.delete.confirm.all": "Do you really want to delete all entries?",
"field.structure.empty": "No entries yet", "field.structure.empty": "No entries yet",
"field.users.empty": "No users selected yet", "field.users.empty": "No users selected yet",
@@ -349,6 +351,8 @@
"license.manage": "Manage your licenses", "license.manage": "Manage your licenses",
"license.register.help": "You received your license code after the purchase via email. Please copy and paste it to register.", "license.register.help": "You received your license code after the purchase via email. Please copy and paste it to register.",
"license.register.label": "Please enter your license code", "license.register.label": "Please enter your license code",
"license.register.domain": "Your license will be registered to <strong>{host}</strong>.",
"license.register.local": "You are about to register your license for your local domain <strong>{host}</strong>. If this site will be deployed to a public domain, please register it there instead. If {host} is the domain you want to license Kirby to, please continue.",
"license.register.success": "Thank you for supporting Kirby", "license.register.success": "Thank you for supporting Kirby",
"license.unregistered": "This is an unregistered demo of Kirby", "license.unregistered": "This is an unregistered demo of Kirby",
"license.unregistered.label": "Unregistered", "license.unregistered.label": "Unregistered",
@@ -393,7 +397,7 @@
"months.april": "April", "months.april": "April",
"months.august": "August", "months.august": "August",
"months.december": "December", "months.december": "December",
"months.february": "Feburary", "months.february": "February",
"months.january": "January", "months.january": "January",
"months.july": "July", "months.july": "July",
"months.june": "June", "months.june": "June",

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

File diff suppressed because one or more lines are too long

View File

@@ -23,7 +23,7 @@ class Collection
protected Api $api; protected Api $api;
protected $data; protected $data;
protected $model; protected $model;
protected $select; protected $select = null;
protected $view; protected $view;
/** /**
@@ -36,7 +36,6 @@ class Collection
$this->api = $api; $this->api = $api;
$this->data = $data; $this->data = $data;
$this->model = $schema['model'] ?? null; $this->model = $schema['model'] ?? null;
$this->select = null;
$this->view = $schema['view'] ?? null; $this->view = $schema['view'] ?? null;
if ($data === null) { if ($data === null) {

View File

@@ -92,11 +92,7 @@ class Model
public function selection(): array public function selection(): array
{ {
$select = $this->select; $select = $this->select;
$select ??= array_keys($this->fields);
if ($select === null) {
$select = array_keys($this->fields);
}
$selection = []; $selection = [];
foreach ($select as $key => $value) { foreach ($select as $key => $value) {

View File

@@ -16,7 +16,7 @@ use TypeError;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class Collection extends BaseCollection class Collection extends BaseCollection
@@ -45,21 +45,22 @@ class Collection extends BaseCollection
/** /**
* Validate the type of every item that is being * Validate the type of every item that is being
* added to the collection. They cneed to have * added to the collection. They need to have
* the class defined by static::TYPE. * the class defined by static::TYPE.
*/ */
public function __set(string $key, $value): void public function __set(string $key, $value): void
{ {
if ( if (is_a($value, static::TYPE) === false) {
is_a($value, static::TYPE) === false
) {
throw new TypeError('Each value in the collection must be an instance of ' . static::TYPE); throw new TypeError('Each value in the collection must be an instance of ' . static::TYPE);
} }
parent::__set($key, $value); parent::__set($key, $value);
} }
public static function factory(array $items) /**
* Creates a collection from a nested array structure
*/
public static function factory(array $items): static
{ {
$collection = new static(); $collection = new static();
$className = static::TYPE; $className = static::TYPE;
@@ -77,7 +78,11 @@ class Collection extends BaseCollection
return $collection; return $collection;
} }
public function render(ModelWithContent $model) /**
* Renders each item with a model and returns
* an array of all rendered results
*/
public function render(ModelWithContent $model): array
{ {
$props = []; $props = [];

View File

@@ -17,7 +17,7 @@ use Kirby\Filesystem\F;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class Config class Config

View File

@@ -11,7 +11,7 @@ namespace Kirby\Blueprint;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class Extension class Extension

View File

@@ -16,7 +16,7 @@ use ReflectionUnionType;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class Factory class Factory
@@ -95,7 +95,7 @@ class Factory
} }
// union types // union types
if (is_a($propType, ReflectionUnionType::class) === true) { if ($propType instanceof ReflectionUnionType) {
return static::forUnionType($propType, $value); return static::forUnionType($propType, $value);
} }

View File

@@ -13,7 +13,7 @@ use Kirby\Cms\ModelWithContent;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class Node class Node
@@ -53,7 +53,6 @@ class Node
return Factory::make(static::class, $props); return Factory::make(static::class, $props);
} }
public static function load(string|array $props): static public static function load(string|array $props): static
{ {
// load by path // load by path

View File

@@ -14,7 +14,7 @@ use Kirby\Toolkit\I18n;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class NodeI18n extends NodeProperty class NodeI18n extends NodeProperty

View File

@@ -11,7 +11,7 @@ namespace Kirby\Blueprint;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class NodeIcon extends NodeString class NodeIcon extends NodeString

View File

@@ -13,7 +13,7 @@ use Kirby\Cms\ModelWithContent;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
abstract class NodeProperty abstract class NodeProperty

View File

@@ -13,7 +13,7 @@ use Kirby\Cms\ModelWithContent;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class NodeString extends NodeProperty class NodeString extends NodeProperty

View File

@@ -14,7 +14,7 @@ use Kirby\Cms\ModelWithContent;
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT * @license https://opensource.org/licenses/MIT
* *
* // TODO: include in test coverage in 3.9 * // TODO: include in test coverage in 3.10
* @codeCoverageIgnore * @codeCoverageIgnore
*/ */
class NodeText extends NodeI18n class NodeText extends NodeI18n

View File

@@ -39,7 +39,8 @@ class Value
* *
* @param int $minutes the number of minutes until the value expires * @param int $minutes the number of minutes until the value expires
* or an absolute UNIX timestamp * or an absolute UNIX timestamp
* @param int $created the UNIX timestamp when the value has been created * @param int|null $created the UNIX timestamp when the value has been created
* (defaults to the current time)
*/ */
public function __construct($value, int $minutes = 0, int|null $created = null) public function __construct($value, int $minutes = 0, int|null $created = null)
{ {

View File

@@ -18,6 +18,7 @@ use Kirby\Http\Router;
use Kirby\Http\Uri; use Kirby\Http\Uri;
use Kirby\Http\Visitor; use Kirby\Http\Visitor;
use Kirby\Session\AutoSession; use Kirby\Session\AutoSession;
use Kirby\Template\Snippet;
use Kirby\Text\KirbyTag; use Kirby\Text\KirbyTag;
use Kirby\Text\KirbyTags; use Kirby\Text\KirbyTags;
use Kirby\Toolkit\A; use Kirby\Toolkit\A;
@@ -1278,6 +1279,11 @@ class App
// set the current locale // set the current locale
$this->setCurrentLanguage($language); $this->setCurrentLanguage($language);
// directly prevent path with incomplete content representation
if (Str::endsWith($path, '.') === true) {
return null;
}
// the site is needed a couple times here // the site is needed a couple times here
$site = $this->site(); $site = $this->site();
@@ -1557,24 +1563,6 @@ class App
return $this; return $this;
} }
/**
* Returns the Environment object
* @deprecated 3.7.0 Use `$kirby->environment()` instead
*
* @return \Kirby\Http\Environment
* @deprecated Will be removed in Kirby 3.9.0
* @todo Remove in 3.9.0
* @codeCoverageIgnore
*/
public function server()
{
// @codeCoverageIgnoreStart
Helpers::deprecated('$kirby->server() has been deprecated and will be removed in Kirby 3.9.0. Use $kirby->environment() instead.');
// @codeCoverageIgnoreEnd
return $this->environment();
}
/** /**
* Initializes and returns the Site object * Initializes and returns the Site object
* *
@@ -1630,15 +1618,20 @@ class App
* @return string|null * @return string|null
* @psalm-return ($return is true ? string : null) * @psalm-return ($return is true ? string : null)
*/ */
public function snippet($name, $data = [], bool $return = true): string|null public function snippet($name, $data = [], bool $return = true, bool $slots = false): Snippet|string|null
{ {
if (is_object($data) === true) { if (is_object($data) === true) {
$data = ['item' => $data]; $data = ['item' => $data];
} }
$snippet = ($this->component('snippet'))($this, $name, array_merge($this->data, $data)); $snippet = ($this->component('snippet'))(
$this,
$name,
array_merge($this->data, $data),
$slots
);
if ($return === true) { if ($return === true || $slots === true) {
return $snippet; return $snippet;
} }
@@ -1661,7 +1654,7 @@ class App
* and return the Template object * and return the Template object
* *
* @internal * @internal
* @return \Kirby\Cms\Template * @return \Kirby\Template\Template
* @param string $name * @param string $name
* @param string $type * @param string $type
* @param string $defaultType * @param string $defaultType

View File

@@ -82,7 +82,8 @@ trait AppCaches
]; ];
} }
$prefix = str_replace(['/', ':'], '_', $this->system()->indexUrl()) . $prefix =
str_replace(['/', ':'], '_', $this->system()->indexUrl()) .
'/' . '/' .
str_replace('.', '/', $key); str_replace('.', '/', $key);

View File

@@ -60,10 +60,7 @@ trait AppUsers
} }
try { try {
// TODO: switch over in 3.9.0 to return $callback($userAfter);
// return $callback($userAfter);
$proxy = new AppUsersImpersonateProxy($this);
return $callback->call($proxy, $userAfter);
} catch (Throwable $e) { } catch (Throwable $e) {
throw $e; throw $e;
} finally { } finally {

View File

@@ -1,32 +0,0 @@
<?php
namespace Kirby\Cms;
/**
* Temporary proxy class to ease transition
* of binding the callback for `$kirby->impersonate()`
*
* @package Kirby Cms
* @author Nico Hoffmann <nico@getkirby.com>,
* Lukas Bestle <lukas@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*
* @internal
* @deprecated Will be removed in Kirby 3.9.0
* @todo remove in 3.9.0
*/
class AppUsersImpersonateProxy
{
public function __construct(protected App $app)
{
}
public function __call($name, $arguments)
{
Helpers::deprecated('Calling $kirby->' . $name . '() as $this->' . $name . '() has been deprecated inside the $kirby->impersonate() callback function. Use a dedicated $kirby object for your call instead of $this. In Kirby 3.9.0 $this will no longer refer to the $kirby object, but the current context of the callback function.');
return $this->app->$name(...$arguments);
}
}

View File

@@ -15,6 +15,7 @@ use Kirby\Http\Idn;
use Kirby\Http\Request\Auth\BasicAuth; use Kirby\Http\Request\Auth\BasicAuth;
use Kirby\Session\Session; use Kirby\Session\Session;
use Kirby\Toolkit\A; use Kirby\Toolkit\A;
use SensitiveParameter;
use Throwable; use Throwable;
/** /**
@@ -381,17 +382,16 @@ class Auth
/** /**
* Login a user by email and password * Login a user by email and password
* *
* @param string $email
* @param string $password
* @param bool $long
* @return \Kirby\Cms\User
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid * @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/ */
public function login(string $email, string $password, bool $long = false) public function login(
{ string $email,
#[SensitiveParameter]
string $password,
bool $long = false
): User {
// session options // session options
$options = [ $options = [
'createMode' => 'cookie', 'createMode' => 'cookie',
@@ -412,17 +412,16 @@ class Auth
* Login a user by email, password and auth challenge * Login a user by email, password and auth challenge
* @since 3.5.0 * @since 3.5.0
* *
* @param string $email
* @param string $password
* @param bool $long
* @return \Kirby\Cms\Auth\Status
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid * @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/ */
public function login2fa(string $email, string $password, bool $long = false) public function login2fa(
{ string $email,
#[SensitiveParameter]
string $password,
bool $long = false
): Status {
$this->validatePassword($email, $password); $this->validatePassword($email, $password);
return $this->createChallenge($email, $long, '2fa'); return $this->createChallenge($email, $long, '2fa');
} }
@@ -516,16 +515,15 @@ class Auth
* Validates the user credentials and returns the user object on success; * Validates the user credentials and returns the user object on success;
* otherwise logs the failed attempt * otherwise logs the failed attempt
* *
* @param string $email
* @param string $password
* @return \Kirby\Cms\User
*
* @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off
* @throws \Kirby\Exception\NotFoundException If the email was invalid * @throws \Kirby\Exception\NotFoundException If the email was invalid
* @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`)
*/ */
public function validatePassword(string $email, string $password) public function validatePassword(
{ string $email,
#[SensitiveParameter]
string $password
): User {
$email = Idn::decodeEmail($email); $email = Idn::decodeEmail($email);
try { try {
@@ -798,8 +796,10 @@ class Auth
* @throws \Kirby\Exception\InvalidArgumentException If no authentication challenge is active * @throws \Kirby\Exception\InvalidArgumentException If no authentication challenge is active
* @throws \Kirby\Exception\LogicException If the authentication challenge is invalid * @throws \Kirby\Exception\LogicException If the authentication challenge is invalid
*/ */
public function verifyChallenge(string $code) public function verifyChallenge(
{ #[SensitiveParameter]
string $code
) {
try { try {
$session = $this->kirby->session(); $session = $this->kirby->session();

View File

@@ -3,6 +3,7 @@
namespace Kirby\Cms\Auth; namespace Kirby\Cms\Auth;
use Kirby\Cms\User; use Kirby\Cms\User;
use SensitiveParameter;
/** /**
* Template class for authentication challenges * Template class for authentication challenges
@@ -48,8 +49,11 @@ abstract class Challenge
* @param string $code Code to verify * @param string $code Code to verify
* @return bool * @return bool
*/ */
public static function verify(User $user, string $code): bool public static function verify(
{ User $user,
#[SensitiveParameter]
string $code
): bool {
$hash = $user->kirby()->session()->get('kirby.challenge.code'); $hash = $user->kirby()->session()->get('kirby.challenge.code');
if (is_string($hash) !== true) { if (is_string($hash) !== true) {
return false; return false;

View File

@@ -115,7 +115,8 @@ class ContentTranslation
*/ */
public function exists(): bool public function exists(): bool
{ {
return empty($this->content) === false || return
empty($this->content) === false ||
file_exists($this->contentFile()) === true; file_exists($this->contentFile()) === true;
} }

View File

@@ -132,7 +132,7 @@ class Email
* *
* @param string $name Template name * @param string $name Template name
* @param string|null $type `html` or `text` * @param string|null $type `html` or `text`
* @return \Kirby\Cms\Template * @return \Kirby\Template\Template
*/ */
protected function getTemplate(string $name, string $type = null) protected function getTemplate(string $name, string $type = null)
{ {

View File

@@ -169,11 +169,12 @@ trait FileActions
* way of generating files. * way of generating files.
* *
* @param array $props * @param array $props
* @param bool $move If set to `true`, the source will be deleted
* @return static * @return static
* @throws \Kirby\Exception\InvalidArgumentException * @throws \Kirby\Exception\InvalidArgumentException
* @throws \Kirby\Exception\LogicException * @throws \Kirby\Exception\LogicException
*/ */
public static function create(array $props) public static function create(array $props, bool $move = false)
{ {
if (isset($props['source'], $props['parent']) === false) { if (isset($props['source'], $props['parent']) === false) {
throw new InvalidArgumentException('Please provide the "source" and "parent" props for the File'); throw new InvalidArgumentException('Please provide the "source" and "parent" props for the File');
@@ -204,12 +205,16 @@ trait FileActions
$file = $file->clone(['content' => $form->strings(true)]); $file = $file->clone(['content' => $form->strings(true)]);
// run the hook // run the hook
return $file->commit('create', compact('file', 'upload'), function ($file, $upload) { $arguments = compact('file', 'upload');
return $file->commit('create', $arguments, function ($file, $upload) use ($move) {
// remove all public versions, lock and clear UUID cache // remove all public versions, lock and clear UUID cache
$file->unpublish(); $file->unpublish();
// only move the original source if intended
$method = $move === true ? 'move' : 'copy';
// overwrite the original // overwrite the original
if (F::copy($upload->root(), $file->root(), true) !== true) { if (F::$method($upload->root(), $file->root(), true) !== true) {
throw new LogicException('The file could not be created'); throw new LogicException('The file could not be created');
} }
@@ -280,10 +285,11 @@ trait FileActions
* source. * source.
* *
* @param string $source * @param string $source
* @param bool $move If set to `true`, the source will be deleted
* @return static * @return static
* @throws \Kirby\Exception\LogicException * @throws \Kirby\Exception\LogicException
*/ */
public function replace(string $source) public function replace(string $source, bool $move = false)
{ {
$file = $this->clone(); $file = $this->clone();
@@ -292,12 +298,15 @@ trait FileActions
'upload' => $file->asset($source) 'upload' => $file->asset($source)
]; ];
return $this->commit('replace', $arguments, function ($file, $upload) { return $this->commit('replace', $arguments, function ($file, $upload) use ($move) {
// delete all public versions // delete all public versions
$file->unpublish(true); $file->unpublish(true);
// only move the original source if intended
$method = $move === true ? 'move' : 'copy';
// overwrite the original // overwrite the original
if (F::copy($upload->root(), $file->root(), true) !== true) { if (F::$method($upload->root(), $file->root(), true) !== true) {
throw new LogicException('The file could not be created'); throw new LogicException('The file could not be created');
} }

View File

@@ -57,16 +57,17 @@ trait HasFiles
* Creates a new file * Creates a new file
* *
* @param array $props * @param array $props
* @param bool $move If set to `true`, the source will be deleted
* @return \Kirby\Cms\File * @return \Kirby\Cms\File
*/ */
public function createFile(array $props) public function createFile(array $props, bool $move = false)
{ {
$props = array_merge($props, [ $props = array_merge($props, [
'parent' => $this, 'parent' => $this,
'url' => null 'url' => null
]); ]);
return File::create($props); return File::create($props, $move);
} }
/** /**

View File

@@ -26,7 +26,10 @@ class Helpers
*/ */
public static function deprecated(string $message): bool public static function deprecated(string $message): bool
{ {
if (App::instance()->option('debug') === true) { if (
App::instance()->option('debug') === true ||
(defined('KIRBY_TESTING') === true && KIRBY_TESTING === true)
) {
return trigger_error($message, E_USER_DEPRECATED) === true; return trigger_error($message, E_USER_DEPRECATED) === true;
} }

View File

@@ -45,7 +45,8 @@ class Html extends \Kirby\Toolkit\Html
} }
} }
// only valid value for 'rel' is 'alternate stylesheet', if 'title' is given as well // only valid value for 'rel' is 'alternate stylesheet',
// if 'title' is given as well
if ( if (
($options['rel'] ?? '') !== 'alternate stylesheet' || ($options['rel'] ?? '') !== 'alternate stylesheet' ||
($options['title'] ?? '') === '' ($options['title'] ?? '') === ''

View File

@@ -659,11 +659,7 @@ class Language extends Model
public function url(): string public function url(): string
{ {
$url = $this->url; $url = $this->url;
$url ??= '/' . $this->code;
if ($url === null) {
$url = '/' . $this->code;
}
return Url::makeAbsolute($url, $this->kirby()->url()); return Url::makeAbsolute($url, $this->kirby()->url());
} }

View File

@@ -629,14 +629,12 @@ abstract class ModelWithContent extends Model implements Identifiable
]); ]);
// validate the input // validate the input
if ($validate === true) { if ($validate === true && $form->isInvalid() === true) {
if ($form->isInvalid() === true) {
throw new InvalidArgumentException([ throw new InvalidArgumentException([
'fallback' => 'Invalid form with errors', 'fallback' => 'Invalid form with errors',
'details' => $form->errors() 'details' => $form->errors()
]); ]);
} }
}
$arguments = [static::CLASS_ALIAS => $this, 'values' => $form->data(), 'strings' => $form->strings(), 'languageCode' => $languageCode]; $arguments = [static::CLASS_ALIAS => $this, 'values' => $form->data(), 'strings' => $form->strings(), 'languageCode' => $languageCode];
return $this->commit('update', $arguments, function ($model, $values, $strings, $languageCode) { return $this->commit('update', $arguments, function ($model, $values, $strings, $languageCode) {

View File

@@ -96,7 +96,7 @@ class Page extends ModelWithContent
* The template, that should be loaded * The template, that should be loaded
* if it exists * if it exists
* *
* @var \Kirby\Cms\Template * @var \Kirby\Template\Template
*/ */
protected $intendedTemplate; protected $intendedTemplate;
@@ -143,7 +143,7 @@ class Page extends ModelWithContent
/** /**
* The intended page template * The intended page template
* *
* @var \Kirby\Cms\Template * @var \Kirby\Template\Template
*/ */
protected $template; protected $template;
@@ -504,7 +504,7 @@ class Page extends ModelWithContent
* Returns the template that should be * Returns the template that should be
* loaded if it exists. * loaded if it exists.
* *
* @return \Kirby\Cms\Template * @return \Kirby\Template\Template
*/ */
public function intendedTemplate() public function intendedTemplate()
{ {
@@ -1096,7 +1096,7 @@ class Page extends ModelWithContent
/** /**
* @internal * @internal
* @param mixed $type * @param mixed $type
* @return \Kirby\Cms\Template * @return \Kirby\Template\Template
* @throws \Kirby\Exception\NotFoundException If the content representation cannot be found * @throws \Kirby\Exception\NotFoundException If the content representation cannot be found
*/ */
public function representation($type) public function representation($type)
@@ -1277,13 +1277,13 @@ class Page extends ModelWithContent
public function slug(string $languageCode = null): string public function slug(string $languageCode = null): string
{ {
if ($this->kirby()->multilang() === true) { if ($this->kirby()->multilang() === true) {
if ($languageCode === null) { $languageCode ??= $this->kirby()->languageCode();
$languageCode = $this->kirby()->languageCode();
}
$defaultLanguageCode = $this->kirby()->defaultLanguage()->code(); $defaultLanguageCode = $this->kirby()->defaultLanguage()->code();
if ($languageCode !== $defaultLanguageCode && $translation = $this->translations()->find($languageCode)) { if (
$languageCode !== $defaultLanguageCode &&
$translation = $this->translations()->find($languageCode)
) {
return $translation->slug() ?? $this->slug; return $translation->slug() ?? $this->slug;
} }
} }
@@ -1313,7 +1313,7 @@ class Page extends ModelWithContent
/** /**
* Returns the final template * Returns the final template
* *
* @return \Kirby\Cms\Template * @return \Kirby\Template\Template
*/ */
public function template() public function template()
{ {

View File

@@ -223,7 +223,7 @@ trait PageActions
throw new InvalidArgumentException('Use the changeSlug method to change the slug for the default language'); throw new InvalidArgumentException('Use the changeSlug method to change the slug for the default language');
} }
$arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $languageCode]; $arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $language->code()];
return $this->commit('changeSlug', $arguments, function ($page, $slug, $languageCode) { return $this->commit('changeSlug', $arguments, function ($page, $slug, $languageCode) {
// remove the slug if it's the same as the folder name // remove the slug if it's the same as the folder name
if ($slug === $page->uid()) { if ($slug === $page->uid()) {
@@ -620,9 +620,7 @@ trait PageActions
->count(); ->count();
// default positioning at the end // default positioning at the end
if ($num === null) { $num ??= $max;
$num = $max;
}
// avoid zeros or negative numbers // avoid zeros or negative numbers
if ($num < 1) { if ($num < 1) {

View File

@@ -255,9 +255,7 @@ class Plugin extends Model
} }
} }
if ($option === null) { $option ??= $kirby->option('updates') ?? true;
$option = $kirby->option('updates') ?? true;
}
if ($option !== true) { if ($option !== true) {
return null; return null;

View File

@@ -31,63 +31,37 @@ use Throwable;
*/ */
class System class System
{ {
/**
* @var \Kirby\Cms\App
*/
protected $app;
// cache // cache
protected UpdateStatus|null $updateStatus = null; protected UpdateStatus|null $updateStatus = null;
/** public function __construct(protected App $app)
* @param \Kirby\Cms\App $app
*/
public function __construct(App $app)
{ {
$this->app = $app;
// try to create all folders that could be missing // try to create all folders that could be missing
$this->init(); $this->init();
} }
/**
* Improved `var_dump` output
*
* @return array
*/
public function __debugInfo(): array
{
return $this->toArray();
}
/** /**
* Check for a writable accounts folder * Check for a writable accounts folder
*
* @return bool
*/ */
public function accounts(): bool public function accounts(): bool
{ {
return is_writable($this->app->root('accounts')); return is_writable($this->app->root('accounts')) === true;
} }
/** /**
* Check for a writable content folder * Check for a writable content folder
*
* @return bool
*/ */
public function content(): bool public function content(): bool
{ {
return is_writable($this->app->root('content')); return is_writable($this->app->root('content')) === true;
} }
/** /**
* Check for an existing curl extension * Check for an existing curl extension
*
* @return bool
*/ */
public function curl(): bool public function curl(): bool
{ {
return extension_loaded('curl'); return extension_loaded('curl') === true;
} }
/** /**
@@ -96,7 +70,6 @@ class System
* root. Otherwise it will return null. * root. Otherwise it will return null.
* *
* @param string $folder 'git', 'content', 'site', 'kirby' * @param string $folder 'git', 'content', 'site', 'kirby'
* @return string|null
*/ */
public function exposedFileUrl(string $folder): string|null public function exposedFileUrl(string $folder): string|null
{ {
@@ -142,19 +115,20 @@ class System
* root. Otherwise it will return null. * root. Otherwise it will return null.
* *
* @param string $folder 'git', 'content', 'site', 'kirby' * @param string $folder 'git', 'content', 'site', 'kirby'
* @return string|null
*/ */
public function folderUrl(string $folder): string|null public function folderUrl(string $folder): string|null
{ {
$index = $this->app->root('index'); $index = $this->app->root('index');
$root = match ($folder) {
'git' => $index . '/.git',
default => $this->app->root($folder)
};
if ($folder === 'git') { if (
$root = $index . '/.git'; $root === null ||
} else { is_dir($root) === false ||
$root = $this->app->root($folder); is_dir($index) === false
} ) {
if ($root === null || is_dir($root) === false || is_dir($index) === false) {
return null; return null;
} }
@@ -180,22 +154,22 @@ class System
/** /**
* Returns the app's human-readable * Returns the app's human-readable
* index URL without scheme * index URL without scheme
*
* @return string
*/ */
public function indexUrl(): string public function indexUrl(): string
{ {
return $this->app->url('index', true)->setScheme(null)->setSlash(false)->toString(); return $this->app->url('index', true)
->setScheme(null)
->setSlash(false)
->toString();
} }
/** /**
* Create the most important folders * Create the most important folders
* if they don't exist yet * if they don't exist yet
* *
* @return void
* @throws \Kirby\Exception\PermissionException * @throws \Kirby\Exception\PermissionException
*/ */
public function init() public function init(): void
{ {
// init /site/accounts // init /site/accounts
try { try {
@@ -231,18 +205,16 @@ class System
* On a public server the panel.install * On a public server the panel.install
* option must be explicitly set to true * option must be explicitly set to true
* to get the installer up and running. * to get the installer up and running.
*
* @return bool
*/ */
public function isInstallable(): bool public function isInstallable(): bool
{ {
return $this->isLocal() === true || $this->app->option('panel.install', false) === true; return
$this->isLocal() === true ||
$this->app->option('panel.install', false) === true;
} }
/** /**
* Check if Kirby is already installed * Check if Kirby is already installed
*
* @return bool
*/ */
public function isInstalled(): bool public function isInstalled(): bool
{ {
@@ -251,8 +223,6 @@ class System
/** /**
* Check if this is a local installation * Check if this is a local installation
*
* @return bool
*/ */
public function isLocal(): bool public function isLocal(): bool
{ {
@@ -261,8 +231,6 @@ class System
/** /**
* Check if all tests pass * Check if all tests pass
*
* @return bool
*/ */
public function isOk(): bool public function isOk(): bool
{ {
@@ -311,7 +279,9 @@ class System
$pubKey = F::read($this->app->root('kirby') . '/kirby.pub'); $pubKey = F::read($this->app->root('kirby') . '/kirby.pub');
// verify the license signature // verify the license signature
if (openssl_verify(json_encode($data), hex2bin($license['signature']), $pubKey, 'RSA-SHA256') !== 1) { $data = json_encode($data);
$signature = hex2bin($license['signature']);
if (openssl_verify($data, $signature, $pubKey, 'RSA-SHA256') !== 1) {
return false; return false;
} }
@@ -338,9 +308,7 @@ class System
*/ */
protected function licenseUrl(string $url = null): string protected function licenseUrl(string $url = null): string
{ {
if ($url === null) { $url ??= $this->indexUrl();
$url = $this->indexUrl();
}
// remove common "testing" subdomains as well as www. // remove common "testing" subdomains as well as www.
// to ensure that installations of the same site have // to ensure that installations of the same site have
@@ -371,8 +339,6 @@ class System
* Returns the configured UI modes for the login form * Returns the configured UI modes for the login form
* with their respective options * with their respective options
* *
* @return array
*
* @throws \Kirby\Exception\InvalidArgumentException If the configuration is invalid * @throws \Kirby\Exception\InvalidArgumentException If the configuration is invalid
* (only in debug mode) * (only in debug mode)
*/ */
@@ -431,45 +397,38 @@ class System
/** /**
* Check for an existing mbstring extension * Check for an existing mbstring extension
*
* @return bool
*/ */
public function mbString(): bool public function mbString(): bool
{ {
return extension_loaded('mbstring'); return extension_loaded('mbstring') === true;
} }
/** /**
* Check for a writable media folder * Check for a writable media folder
*
* @return bool
*/ */
public function media(): bool public function media(): bool
{ {
return is_writable($this->app->root('media')); return is_writable($this->app->root('media')) === true;
} }
/** /**
* Check for a valid PHP version * Check for a valid PHP version
*
* @return bool
*/ */
public function php(): bool public function php(): bool
{ {
return return
version_compare(PHP_VERSION, '8.0.0', '>=') === true && version_compare(PHP_VERSION, '8.0.0', '>=') === true &&
version_compare(PHP_VERSION, '8.2.0', '<') === true; version_compare(PHP_VERSION, '8.3.0', '<') === true;
} }
/** /**
* Returns a sorted collection of all * Returns a sorted collection of all
* installed plugins * installed plugins
*
* @return \Kirby\Cms\Collection
*/ */
public function plugins() public function plugins(): Collection
{ {
return (new Collection(App::instance()->plugins()))->sortBy('name', 'asc'); $plugins = new Collection($this->app->plugins());
return $plugins->sortBy('name', 'asc');
} }
/** /**
@@ -477,24 +436,17 @@ class System
* and adds it to the .license file in the config * and adds it to the .license file in the config
* folder if possible. * folder if possible.
* *
* @param string|null $license
* @param string|null $email
* @return bool
* @throws \Kirby\Exception\Exception * @throws \Kirby\Exception\Exception
* @throws \Kirby\Exception\InvalidArgumentException * @throws \Kirby\Exception\InvalidArgumentException
*/ */
public function register(string $license = null, string $email = null): bool public function register(string $license = null, string $email = null): bool
{ {
if (Str::startsWith($license, 'K3-PRO-') === false) { if (Str::startsWith($license, 'K3-PRO-') === false) {
throw new InvalidArgumentException([ throw new InvalidArgumentException(['key' => 'license.format']);
'key' => 'license.format'
]);
} }
if (V::email($email) === false) { if (V::email($email) === false) {
throw new InvalidArgumentException([ throw new InvalidArgumentException(['key' => 'license.email']);
'key' => 'license.email'
]);
} }
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
@@ -534,8 +486,6 @@ class System
/** /**
* Check for a valid server environment * Check for a valid server environment
*
* @return bool
*/ */
public function server(): bool public function server(): bool
{ {
@@ -544,8 +494,6 @@ class System
/** /**
* Returns the detected server software * Returns the detected server software
*
* @return string|null
*/ */
public function serverSoftware(): string|null public function serverSoftware(): string|null
{ {
@@ -566,18 +514,14 @@ class System
/** /**
* Check for a writable sessions folder * Check for a writable sessions folder
*
* @return bool
*/ */
public function sessions(): bool public function sessions(): bool
{ {
return is_writable($this->app->root('sessions')); return is_writable($this->app->root('sessions')) === true;
} }
/** /**
* Get an status array of all checks * Get an status array of all checks
*
* @return array
*/ */
public function status(): array public function status(): array
{ {
@@ -597,23 +541,18 @@ class System
* Returns the site's title as defined in the * Returns the site's title as defined in the
* content file or `site.yml` blueprint * content file or `site.yml` blueprint
* @since 3.6.0 * @since 3.6.0
*
* @return string
*/ */
public function title(): string public function title(): string
{ {
$site = $this->app->site(); $site = $this->app->site();
if ($site->title()->isNotEmpty()) { if ($site->title()->isNotEmpty() === true) {
return $site->title()->value(); return $site->title()->value();
} }
return $site->blueprint()->title(); return $site->blueprint()->title();
} }
/**
* @return array
*/
public function toArray(): array public function toArray(): array
{ {
return $this->status(); return $this->status();
@@ -633,7 +572,9 @@ class System
} }
$kirby = $this->app; $kirby = $this->app;
$option = $kirby->option('updates.kirby') ?? $kirby->option('updates') ?? true; $option =
$kirby->option('updates.kirby') ??
$kirby->option('updates', true);
if ($option === false) { if ($option === false) {
return null; return null;
@@ -648,11 +589,8 @@ class System
/** /**
* Upgrade to the new folder separator * Upgrade to the new folder separator
*
* @param string $root
* @return void
*/ */
public static function upgradeContent(string $root) public static function upgradeContent(string $root): void
{ {
$index = Dir::read($root); $index = Dir::read($root);
@@ -666,4 +604,12 @@ class System
} }
} }
} }
/**
* Improved `var_dump` output
*/
public function __debugInfo(): array
{
return $this->toArray();
}
} }

View File

@@ -435,9 +435,7 @@ class UpdateStatus
// verify that we found at least one possible version; // verify that we found at least one possible version;
// otherwise try the `$maxVersion` as a last chance before // otherwise try the `$maxVersion` as a last chance before
// concluding at the top that we cannot solve the task // concluding at the top that we cannot solve the task
if ($incidentVersion === null) { $incidentVersion ??= $maxVersion;
$incidentVersion = $maxVersion;
}
// we need a version that fixes all vulnerabilities, so use the // we need a version that fixes all vulnerabilities, so use the
// "largest of the smallest" fixed versions // "largest of the smallest" fixed versions

View File

@@ -10,6 +10,7 @@ use Kirby\Filesystem\F;
use Kirby\Panel\User as Panel; use Kirby\Panel\User as Panel;
use Kirby\Session\Session; use Kirby\Session\Session;
use Kirby\Toolkit\Str; use Kirby\Toolkit\Str;
use SensitiveParameter;
/** /**
* The `$user` object represents a * The `$user` object represents a
@@ -274,11 +275,11 @@ class User extends ModelWithContent
* which will leave it as `null` * which will leave it as `null`
* *
* @internal * @internal
* @param string|null $password
* @return string|null
*/ */
public static function hashPassword($password): string|null public static function hashPassword(
{ #[SensitiveParameter]
string $password = null
): string|null {
if ($password !== null) { if ($password !== null) {
$password = password_hash($password, PASSWORD_DEFAULT); $password = password_hash($password, PASSWORD_DEFAULT);
} }
@@ -372,7 +373,8 @@ class User extends ModelWithContent
*/ */
public function isLastAdmin(): bool public function isLastAdmin(): bool
{ {
return $this->role()->isAdmin() === true && return
$this->role()->isAdmin() === true &&
$this->kirby()->users()->filter('role', 'admin')->count() <= 1; $this->kirby()->users()->filter('role', 'admin')->count() <= 1;
} }
@@ -410,12 +412,13 @@ class User extends ModelWithContent
/** /**
* Logs the user in * Logs the user in
* *
* @param string $password
* @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in * @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in
* @return bool
*/ */
public function login(string $password, $session = null): bool public function login(
{ #[SensitiveParameter]
string $password,
$session = null
): bool {
$this->validatePassword($password); $this->validatePassword($password);
$this->loginPasswordless($session); $this->loginPasswordless($session);
@@ -750,11 +753,12 @@ class User extends ModelWithContent
/** /**
* Sets the user's password hash * Sets the user's password hash
* *
* @param string $password|null
* @return $this * @return $this
*/ */
protected function setPassword(string $password = null) protected function setPassword(
{ #[SensitiveParameter]
string $password = null
): static {
$this->password = $password; $this->password = $password;
return $this; return $this;
} }
@@ -848,15 +852,14 @@ class User extends ModelWithContent
/** /**
* Compares the given password with the stored one * 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\NotFoundException If the user has no password
* @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid * @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid
* or does not match the user password * or does not match the user password
*/ */
public function validatePassword(string $password = null): bool public function validatePassword(
{ #[SensitiveParameter]
string $password = null
): bool {
if (empty($this->password()) === true) { if (empty($this->password()) === true) {
throw new NotFoundException(['key' => 'user.password.undefined']); throw new NotFoundException(['key' => 'user.password.undefined']);
} }

View File

@@ -11,6 +11,7 @@ use Kirby\Filesystem\F;
use Kirby\Form\Form; use Kirby\Form\Form;
use Kirby\Http\Idn; use Kirby\Http\Idn;
use Kirby\Toolkit\Str; use Kirby\Toolkit\Str;
use SensitiveParameter;
use Throwable; use Throwable;
/** /**
@@ -102,12 +103,11 @@ trait UserActions
/** /**
* Changes the user password * Changes the user password
*
* @param string $password
* @return static
*/ */
public function changePassword(string $password) public function changePassword(
{ #[SensitiveParameter]
string $password
): static {
return $this->commit('changePassword', ['user' => $this, 'password' => $password], function ($user, $password) { return $this->commit('changePassword', ['user' => $this, 'password' => $password], function ($user, $password) {
$user = $user->clone([ $user = $user->clone([
'password' => $password = User::hashPassword($password) 'password' => $password = User::hashPassword($password)
@@ -379,12 +379,11 @@ trait UserActions
/** /**
* Writes the password to disk * Writes the password to disk
*
* @param string|null $password
* @return bool
*/ */
protected function writePassword(string $password = null): bool protected function writePassword(
{ #[SensitiveParameter]
string $password = null
): bool {
return F::write($this->root() . '/.htpasswd', $password); return F::write($this->root() . '/.htpasswd', $password);
} }
} }

View File

@@ -8,6 +8,7 @@ use Kirby\Exception\LogicException;
use Kirby\Exception\PermissionException; use Kirby\Exception\PermissionException;
use Kirby\Toolkit\Str; use Kirby\Toolkit\Str;
use Kirby\Toolkit\V; use Kirby\Toolkit\V;
use SensitiveParameter;
/** /**
* Validators for all user actions * Validators for all user actions
@@ -83,13 +84,13 @@ class UserRules
/** /**
* Validates if the password can be changed * Validates if the password can be changed
* *
* @param \Kirby\Cms\User $user
* @param string $password
* @return bool
* @throws \Kirby\Exception\PermissionException If the user is not allowed to change the password * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the password
*/ */
public static function changePassword(User $user, string $password): bool public static function changePassword(
{ User $user,
#[SensitiveParameter]
string $password
): bool {
if ($user->permissions()->changePassword() !== true) { if ($user->permissions()->changePassword() !== true) {
throw new PermissionException([ throw new PermissionException([
'key' => 'user.changePassword.permission', 'key' => 'user.changePassword.permission',
@@ -193,13 +194,14 @@ class UserRules
} }
// check user permissions (if not on install) // check user permissions (if not on install)
if ($user->kirby()->users()->count() > 0) { if (
if ($user->permissions()->create() !== true) { $user->kirby()->users()->count() > 0 &&
$user->permissions()->create() !== true
) {
throw new PermissionException([ throw new PermissionException([
'key' => 'user.create.permission' 'key' => 'user.create.permission'
]); ]);
} }
}
return true; return true;
} }
@@ -332,13 +334,13 @@ class UserRules
/** /**
* Validates a password * Validates a password
* *
* @param \Kirby\Cms\User $user
* @param string $password
* @return bool
* @throws \Kirby\Exception\InvalidArgumentException If the password is too short * @throws \Kirby\Exception\InvalidArgumentException If the password is too short
*/ */
public static function validPassword(User $user, string $password): bool public static function validPassword(
{ User $user,
#[SensitiveParameter]
string $password
): bool {
if (Str::length($password ?? null) < 8) { if (Str::length($password ?? null) < 8) {
throw new InvalidArgumentException([ throw new InvalidArgumentException([
'key' => 'user.password.invalid', 'key' => 'user.password.invalid',

View File

@@ -55,7 +55,8 @@ class Data
// find a handler or alias // find a handler or alias
$alias = static::$aliases[$type] ?? null; $alias = static::$aliases[$type] ?? null;
$handler = static::$handlers[$type] ?? $handler =
static::$handlers[$type] ??
($alias ? static::$handlers[$alias] ?? null : null); ($alias ? static::$handlers[$alias] ?? null : null);
if ($handler === null || class_exists($handler) === false) { if ($handler === null || class_exists($handler) === false) {

View File

@@ -6,6 +6,7 @@ use Kirby\Blueprint\Node;
use Kirby\Cms\ModelWithContent; use Kirby\Cms\ModelWithContent;
use Kirby\Option\Options; use Kirby\Option\Options;
use Kirby\Option\OptionsApi; use Kirby\Option\OptionsApi;
use Kirby\Option\OptionsProvider;
use Kirby\Option\OptionsQuery; use Kirby\Option\OptionsQuery;
/** /**
@@ -20,7 +21,18 @@ use Kirby\Option\OptionsQuery;
class FieldOptions extends Node class FieldOptions extends Node
{ {
public function __construct( public function __construct(
public Options|OptionsApi|OptionsQuery|null $options = null /**
* The option source, either a fixed collection or
* a dynamic provider
*/
public Options|OptionsProvider|null $options = null,
/**
* Whether to escape special HTML characters in
* the option text for safe output in the Panel;
* only set to `false` if the text is later escaped!
*/
public bool $safeMode = true
) { ) {
} }
@@ -31,7 +43,7 @@ class FieldOptions extends Node
return parent::defaults(); return parent::defaults();
} }
public static function factory(array $props): static public static function factory(array $props, bool $safeMode = true): static
{ {
$options = match ($props['type']) { $options = match ($props['type']) {
'api' => OptionsApi::factory($props), 'api' => OptionsApi::factory($props),
@@ -39,20 +51,23 @@ class FieldOptions extends Node
default => Options::factory($props['options'] ?? []) default => Options::factory($props['options'] ?? [])
}; };
return new static($options); return new static($options, $safeMode);
} }
public static function polyfill(array $props = []): array public static function polyfill(array $props = []): array
{ {
if (is_string($props['options'] ?? null) === true) { if (is_string($props['options'] ?? null) === true) {
$props['options'] = match ($props['options']) { $props['options'] = match ($props['options']) {
'api' => ['type' => 'api'] + 'api' =>
['type' => 'api'] +
OptionsApi::polyfill($props['api'] ?? null), OptionsApi::polyfill($props['api'] ?? null),
'query' => ['type' => 'query'] + 'query' =>
['type' => 'query'] +
OptionsQuery::polyfill($props['query'] ?? null), OptionsQuery::polyfill($props['query'] ?? null),
default => [ 'type' => 'query', 'query' => $props['options']] default =>
[ 'type' => 'query', 'query' => $props['options']]
}; };
} }
@@ -82,8 +97,8 @@ class FieldOptions extends Node
return $this->options; return $this->options;
} }
// resolve OptionsApi or OptionsQuery to Options // resolve OptionsProvider (OptionsApi or OptionsQuery) to Options
return $this->options = $this->options->resolve($model); return $this->options = $this->options->resolve($model, $this->safeMode);
} }
public function render(ModelWithContent $model): array public function render(ModelWithContent $model): array

View File

@@ -48,12 +48,16 @@ class Dir
/** /**
* Copy the directory to a new destination * Copy the directory to a new destination
*
* @param array|false $ignore List of full paths to skip during copying
* or `false` to copy all files, including
* those listed in `Dir::$ignore`
*/ */
public static function copy( public static function copy(
string $dir, string $dir,
string $target, string $target,
bool $recursive = true, bool $recursive = true,
array $ignore = [] array|bool $ignore = []
): bool { ): bool {
if (is_dir($dir) === false) { if (is_dir($dir) === false) {
throw new Exception('The directory "' . $dir . '" does not exist'); throw new Exception('The directory "' . $dir . '" does not exist');
@@ -67,10 +71,13 @@ class Dir
throw new Exception('The target directory "' . $target . '" could not be created'); throw new Exception('The target directory "' . $target . '" could not be created');
} }
foreach (static::read($dir) as $name) { foreach (static::read($dir, $ignore === false ? [] : null) as $name) {
$root = $dir . '/' . $name; $root = $dir . '/' . $name;
if (in_array($root, $ignore) === true) { if (
is_array($ignore) === true &&
in_array($root, $ignore) === true
) {
continue; continue;
} }

View File

@@ -514,7 +514,14 @@ class F
static::remove($newRoot); static::remove($newRoot);
} }
// actually move the file if it exists $directory = dirname($newRoot);
// create the parent directory if it does not exist
if (is_dir($directory) === false) {
Dir::make($directory, true);
}
// actually move the file
if (rename($oldRoot, $newRoot) !== true) { if (rename($oldRoot, $newRoot) !== true) {
return false; return false;
} }

View File

@@ -62,7 +62,10 @@ class Field extends Component
public function __construct(string $type, array $attrs = [], ?Fields $formFields = null) public function __construct(string $type, array $attrs = [], ?Fields $formFields = null)
{ {
if (isset(static::$types[$type]) === false) { if (isset(static::$types[$type]) === false) {
throw new InvalidArgumentException('The field type "' . $type . '" does not exist'); throw new InvalidArgumentException([
'key' => 'field.type.missing',
'data' => ['name' => $attrs['name'] ?? '-', 'type' => $type]
]);
} }
if (isset($attrs['model']) === false) { if (isset($attrs['model']) === false) {

View File

@@ -324,9 +324,7 @@ class Form
return $fields; return $fields;
} }
if ($language === null) { $language ??= $kirby->language()->code();
$language = $kirby->language()->code();
}
if ($language !== $kirby->defaultLanguage()->code()) { if ($language !== $kirby->defaultLanguage()->code()) {
foreach ($fields as $fieldName => $fieldProps) { foreach ($fields as $fieldName => $fieldProps) {

View File

@@ -195,7 +195,7 @@ class Environment
* Sets the host name, port, path and protocol from the * Sets the host name, port, path and protocol from the
* fixed list of allowed URLs * fixed list of allowed URLs
*/ */
protected function detectAllowed(array|string|object $allowed): void protected function detectAllowed(array|string $allowed): void
{ {
$allowed = A::wrap($allowed); $allowed = A::wrap($allowed);

View File

@@ -2,6 +2,8 @@
namespace Kirby\Http\Request; namespace Kirby\Http\Request;
use SensitiveParameter;
/** /**
* Base class for auth types * Base class for auth types
* *
@@ -22,8 +24,10 @@ abstract class Auth
/** /**
* Constructor * Constructor
*/ */
public function __construct(string $data) public function __construct(
{ #[SensitiveParameter]
string $data
) {
$this->data = $data; $this->data = $data;
} }

View File

@@ -4,6 +4,7 @@ namespace Kirby\Http\Request\Auth;
use Kirby\Http\Request\Auth; use Kirby\Http\Request\Auth;
use Kirby\Toolkit\Str; use Kirby\Toolkit\Str;
use SensitiveParameter;
/** /**
* HTTP basic authentication data * HTTP basic authentication data
@@ -20,8 +21,10 @@ class BasicAuth extends Auth
protected string|null $password; protected string|null $password;
protected string|null $username; protected string|null $username;
public function __construct(string $data) public function __construct(
{ #[SensitiveParameter]
string $data
) {
parent::__construct($data); parent::__construct($data);
$this->credentials = base64_decode($data); $this->credentials = base64_decode($data);

View File

@@ -22,7 +22,7 @@ class Files
/** /**
* Sanitized array of all received files * Sanitized array of all received files
*/ */
protected array $files; protected array $files = [];
/** /**
* Creates a new Files object * Creates a new Files object
@@ -31,11 +31,7 @@ class Files
*/ */
public function __construct(array|null $files = null) public function __construct(array|null $files = null)
{ {
if ($files === null) { $files ??= $_FILES;
$files = $_FILES;
}
$this->files = [];
foreach ($files as $key => $file) { foreach ($files as $key => $file) {
if (is_array($file['name'])) { if (is_array($file['name'])) {

View File

@@ -178,6 +178,7 @@ class Response
* @since 3.7.0 * @since 3.7.0
* *
* @codeCoverageIgnore * @codeCoverageIgnore
* @todo Change return type to `never` once support for PHP 8.0 is dropped
*/ */
public static function go(string $url = '/', int $code = 302): void public static function go(string $url = '/', int $code = 302): void
{ {

View File

@@ -117,7 +117,7 @@ class Router
if ($callback) { if ($callback) {
$result = $callback($route); $result = $callback($route);
} else { } else {
$result = $route?->action()->call($route, ...$route->arguments()); $result = $route->action()->call($route, ...$route->arguments());
} }
$loop = false; $loop = false;
@@ -150,7 +150,7 @@ class Router
* find matches and return all the found * find matches and return all the found
* arguments in the path. * arguments in the path.
*/ */
public function find(string $path, string $method, array|null $ignore = null): Route|null public function find(string $path, string $method, array|null $ignore = null): Route
{ {
if (isset($this->routes[$method]) === false) { if (isset($this->routes[$method]) === false) {
throw new InvalidArgumentException('Invalid routing method: ' . $method, 400); throw new InvalidArgumentException('Invalid routing method: ' . $method, 400);

View File

@@ -5,6 +5,7 @@ namespace Kirby\Http;
use Kirby\Cms\App; use Kirby\Cms\App;
use Kirby\Exception\InvalidArgumentException; use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\Properties; use Kirby\Toolkit\Properties;
use SensitiveParameter;
use Throwable; use Throwable;
/** /**
@@ -326,8 +327,10 @@ class Uri
/** /**
* @return $this * @return $this
*/ */
public function setPassword(string|null $password = null): static public function setPassword(
{ #[SensitiveParameter]
string|null $password = null
): static {
$this->password = $password; $this->password = $password;
return $this; return $this;
} }

View File

@@ -2,6 +2,8 @@
namespace Kirby\Image; namespace Kirby\Image;
use Kirby\Toolkit\Str;
/** /**
* The Dimension class is used to provide additional * The Dimension class is used to provide additional
* methods for images and possibly other objects with * methods for images and possibly other objects with
@@ -253,12 +255,28 @@ class Dimensions
if ($xml !== false) { if ($xml !== false) {
$attr = $xml->attributes(); $attr = $xml->attributes();
$width = (int)($attr->width);
$height = (int)($attr->height); $rawWidth = $attr->width;
if (($width === 0 || $height === 0) && empty($attr->viewBox) === false) { $width = (int)$rawWidth;
$rawHeight = $attr->height;
$height = (int)$rawHeight;
// use viewbox values if direct attributes are 0
// or based on percentages
if (empty($attr->viewBox) === false) {
$box = explode(' ', $attr->viewBox); $box = explode(' ', $attr->viewBox);
$width = (int)($box[2] ?? 0);
$height = (int)($box[3] ?? 0); // when using viewbox values, make sure to subtract
// first two box values from last two box values
// to retrieve the absolute dimensions
if (Str::endsWith($rawWidth, '%') === true || $width === 0) {
$width = (int)($box[2] ?? 0) - (int)($box[0] ?? 0);
}
if (Str::endsWith($rawHeight, '%') === true || $height === 0) {
$height = (int)($box[3] ?? 0) - (int)($box[1] ?? 0);
}
} }
} }

View File

@@ -215,7 +215,8 @@ class Exif
*/ */
protected function parseFocalLength(): string|null protected function parseFocalLength(): string|null
{ {
return $this->data['FocalLength'] ?? return
$this->data['FocalLength'] ??
$this->data['FocalLengthIn35mmFilm'] ?? $this->data['FocalLengthIn35mmFilm'] ??
null; null;
} }

View File

@@ -112,6 +112,12 @@ class Image extends File
*/ */
public function html(array $attr = []): string public function html(array $attr = []): string
{ {
// if no alt text explicitly provided,
// try to infer from model content file
if ($alt = $this->model?->alt()) {
$attr['alt'] ??= $alt;
}
if ($url = $this->url()) { if ($url = $this->url()) {
return Html::img($url, $attr); return Html::img($url, $attr);
} }

View File

@@ -8,7 +8,7 @@ use Kirby\Blueprint\NodeText;
use Kirby\Cms\ModelWithContent; use Kirby\Cms\ModelWithContent;
/** /**
* Option for select fields, radio fields, etc * Option for select fields, radio fields, etc.
* *
* @package Kirby Option * @package Kirby Option
* @author Bastian Allgeier <bastian@getkirby.com> * @author Bastian Allgeier <bastian@getkirby.com>
@@ -19,7 +19,7 @@ use Kirby\Cms\ModelWithContent;
class Option class Option
{ {
public function __construct( public function __construct(
public float|int|string|null $value, public string|int|float|null $value,
public bool $disabled = false, public bool $disabled = false,
public NodeIcon|null $icon = null, public NodeIcon|null $icon = null,
public NodeText|null $info = null, public NodeText|null $info = null,
@@ -28,7 +28,7 @@ class Option
$this->text ??= new NodeText(['en' => $this->value]); $this->text ??= new NodeText(['en' => $this->value]);
} }
public static function factory(float|int|string|null|array $props): static public static function factory(string|int|float|null|array $props): static
{ {
if (is_array($props) === false) { if (is_array($props) === false) {
$props = ['value' => $props]; $props = ['value' => $props];

View File

@@ -6,7 +6,8 @@ use Kirby\Blueprint\Collection;
use Kirby\Cms\ModelWithContent; use Kirby\Cms\ModelWithContent;
/** /**
* Options * Collection of possible options for
* select fields, radio fields, etc.
* *
* @package Kirby Option * @package Kirby Option
* @author Bastian Allgeier <bastian@getkirby.com> * @author Bastian Allgeier <bastian@getkirby.com>
@@ -30,6 +31,7 @@ class Options extends Collection
$collection = new static(); $collection = new static();
foreach ($items as $key => $option) { foreach ($items as $key => $option) {
// convert an associative value => text array into props;
// skip if option is already an array of option props // skip if option is already an array of option props
if ( if (
is_array($option) === false || is_array($option) === false ||

View File

@@ -88,8 +88,12 @@ class OptionsApi extends OptionsProvider
* Creates the actual options by loading * Creates the actual options by loading
* data from the API and resolving it to * data from the API and resolving it to
* the correct text-value entries * the correct text-value entries
*
* @param bool $safeMode Whether to escape special HTML characters in
* the option text for safe output in the Panel;
* only set to `false` if the text is later escaped!
*/ */
public function resolve(ModelWithContent $model): Options public function resolve(ModelWithContent $model, bool $safeMode = true): Options
{ {
// use cached options if present // use cached options if present
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
@@ -101,25 +105,48 @@ class OptionsApi extends OptionsProvider
// apply property defaults // apply property defaults
$this->defaults(); $this->defaults();
// load data from URL and narrow down to queried part // load data from URL and convert from JSON to array
$data = $this->load($model); $data = $this->load($model);
if ($data === null) { if ($data === null) {
throw new NotFoundException('Options could not be loaded from API: ' . $model->toSafeString($this->url)); throw new NotFoundException('Options could not be loaded from API: ' . $model->toSafeString($this->url));
} }
// optionally query a substructure inside the data array
if ($this->query !== null) {
// turn data into Nest so that it can be queried // turn data into Nest so that it can be queried
$data = Nest::create($data); $data = Nest::create($data);
$data = Query::factory($this->query)->resolve($data);
// actually apply the query and turn the result back into an array
$data = Query::factory($this->query)->resolve($data)->toArray();
}
// create options by resolving text and value query strings // create options by resolving text and value query strings
// for each item from the data // for each item from the data
$options = $data->toArray(fn ($item) => [ $options = array_map(
function ($item, $key) use ($model, $safeMode) {
// convert simple `key: value` API data
if (is_string($item) === true) {
$item = [
'key' => $key,
'value' => $item
];
}
$safeMethod = $safeMode === true ? 'toSafeString' : 'toString';
return [
// value is always a raw string // value is always a raw string
'value' => $model->toString($this->value, ['item' => $item]), 'value' => $model->toString($this->value, ['item' => $item]),
// text is only a raw string when using {< >} // text is only a raw string when using {< >}
'text' => $model->toSafeString($this->text, ['item' => $item]), // or when the safe mode is explicitly disabled (select field)
]); 'text' => $model->$safeMethod($this->text, ['item' => $item])
];
},
// separately pass values and keys to have the keys available in the callback
$data,
array_keys($data)
);
// create Options object and render this subsequently // create Options object and render this subsequently
return $this->options = Options::factory($options); return $this->options = Options::factory($options);

View File

@@ -26,5 +26,13 @@ abstract class OptionsProvider
return $this->resolve($model)->render($model); return $this->resolve($model)->render($model);
} }
abstract public function resolve(ModelWithContent $model): Options; /**
* Dynamically determines the actual options and resolves
* them to the correct text-value entries
*
* @param bool $safeMode Whether to escape special HTML characters in
* the option text for safe output in the Panel;
* only set to `false` if the text is later escaped!
*/
abstract public function resolve(ModelWithContent $model, bool $safeMode = true): Options;
} }

View File

@@ -131,8 +131,12 @@ class OptionsQuery extends OptionsProvider
* Creates the actual options by running * Creates the actual options by running
* the query on the model and resolving it to * the query on the model and resolving it to
* the correct text-value entries * the correct text-value entries
*
* @param bool $safeMode Whether to escape special HTML characters in
* the option text for safe output in the Panel;
* only set to `false` if the text is later escaped!
*/ */
public function resolve(ModelWithContent $model): Options public function resolve(ModelWithContent $model, bool $safeMode = true): Options
{ {
// use cached options if present // use cached options if present
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
@@ -159,7 +163,7 @@ class OptionsQuery extends OptionsProvider
} }
// create options array // create options array
$options = $result->toArray(function ($item) use ($model) { $options = $result->toArray(function ($item) use ($model, $safeMode) {
// get defaults based on item type // get defaults based on item type
[$alias, $text, $value] = $this->itemToDefaults($item); [$alias, $text, $value] = $this->itemToDefaults($item);
$data = ['item' => $item, $alias => $item]; $data = ['item' => $item, $alias => $item];
@@ -167,9 +171,10 @@ class OptionsQuery extends OptionsProvider
// value is always a raw string // value is always a raw string
$value = $model->toString($this->value ?? $value, $data); $value = $model->toString($this->value ?? $value, $data);
// text is only a raw string when HTML prop // text is only a raw string when using {< >}
// is explicitly set to true // or when the safe mode is explicitly disabled (select field)
$text = $model->toSafeString($this->text ?? $text, $data); $safeMethod = $safeMode === true ? 'toSafeString' : 'toString';
$text = $model->$safeMethod($this->text ?? $text, $data);
return compact('text', 'value'); return compact('text', 'value');
}); });

View File

@@ -196,7 +196,8 @@ class File extends Model
'rtf' => 'blue-400' 'rtf' => 'blue-400'
]; ];
return $extensions[$this->model->extension()] ?? return
$extensions[$this->model->extension()] ??
$types[$this->model->type()] ?? $types[$this->model->type()] ??
parent::imageDefaults()['color']; parent::imageDefaults()['color'];
} }
@@ -237,7 +238,8 @@ class File extends Model
'md' => 'markdown' 'md' => 'markdown'
]; ];
return $extensions[$this->model->extension()] ?? return
$extensions[$this->model->extension()] ??
$types[$this->model->type()] ?? $types[$this->model->type()] ??
'file'; 'file';
} }

View File

@@ -196,10 +196,7 @@ class Page extends Model
protected function imageSource( protected function imageSource(
string|null $query = null string|null $query = null
): CmsFile|Asset|null { ): CmsFile|Asset|null {
if ($query === null) { $query ??= 'page.image';
$query = 'page.image';
}
return parent::imageSource($query); return parent::imageSource($query);
} }
@@ -234,7 +231,8 @@ class Page extends Model
*/ */
public function position(): int public function position(): int
{ {
return $this->model->num() ?? return
$this->model->num() ??
$this->model->parentModel()->children()->listed()->not($this->model)->count() + 1; $this->model->parentModel()->children()->listed()->not($this->model)->count() + 1;
} }

View File

@@ -38,10 +38,7 @@ class Site extends Model
protected function imageSource( protected function imageSource(
string|null $query = null string|null $query = null
): CmsFile|Asset|null { ): CmsFile|Asset|null {
if ($query === null) { $query ??= 'site.image';
$query = 'site.image';
}
return parent::imageSource($query); return parent::imageSource($query);
} }

View File

@@ -96,7 +96,7 @@ class Element
* Tries to find a single nested element by * Tries to find a single nested element by
* query and otherwise returns null * query and otherwise returns null
*/ */
public function find(string $query): Element|null public function find(string $query): static|null
{ {
if ($result = $this->query($query)[0]) { if ($result = $this->query($query)[0]) {
return new static($result); return new static($result);
@@ -107,6 +107,8 @@ class Element
/** /**
* Returns the inner HTML of the element * Returns the inner HTML of the element
*
* @param array|null $marks List of allowed marks
*/ */
public function innerHtml(array|null $marks = null): string public function innerHtml(array|null $marks = null): string
{ {

View File

@@ -38,18 +38,23 @@ class Argument
$argument = trim(substr($argument, 1, -1)); $argument = trim(substr($argument, 1, -1));
} }
// string with single or double quotes // string with single quotes
if ( if (
(
Str::startsWith($argument, '"') &&
Str::endsWith($argument, '"')
) || (
Str::startsWith($argument, "'") && Str::startsWith($argument, "'") &&
Str::endsWith($argument, "'") Str::endsWith($argument, "'")
)
) { ) {
$string = substr($argument, 1, -1); $string = substr($argument, 1, -1);
$string = str_replace(['\"', "\'"], ['"', "'"], $string); $string = str_replace("\'", "'", $string);
return new static($string);
}
// string with double quotes
if (
Str::startsWith($argument, '"') &&
Str::endsWith($argument, '"')
) {
$string = substr($argument, 1, -1);
$string = str_replace('\"', '"', $string);
return new static($string); return new static($string);
} }

View File

@@ -6,8 +6,8 @@ use Kirby\Toolkit\A;
use Kirby\Toolkit\Collection; use Kirby\Toolkit\Collection;
/** /**
* The Argument class represents a single * The Arguments class helps splitting a
* parameter passed to a method in a chained query * parameter string into processable arguments
* *
* @package Kirby Query * @package Kirby Query
* @author Nico Hoffmann <nico@getkirby.com> * @author Nico Hoffmann <nico@getkirby.com>
@@ -26,7 +26,8 @@ class Arguments extends Collection
// skip all matches inside of single quotes // skip all matches inside of single quotes
public const NO_SLQU = '\'(?:[^\'\\\\]|\\\\.)*\'(*SKIP)(*FAIL)'; public const NO_SLQU = '\'(?:[^\'\\\\]|\\\\.)*\'(*SKIP)(*FAIL)';
// skip all matches inside of any of the above skip groups // skip all matches inside of any of the above skip groups
public const OUTSIDE = self::NO_PNTH . '|' . self::NO_SQBR . '|' . public const OUTSIDE =
self::NO_PNTH . '|' . self::NO_SQBR . '|' .
self::NO_DLQU . '|' . self::NO_SLQU; self::NO_DLQU . '|' . self::NO_SLQU;
/** /**

View File

@@ -22,6 +22,9 @@ class Expression
) { ) {
} }
/**
* Parses an expression string into its parts
*/
public static function factory(string $expression, Query $parent = null): static|Segments public static function factory(string $expression, Query $parent = null): static|Segments
{ {
// split into different expression parts and operators // split into different expression parts and operators

View File

@@ -124,7 +124,6 @@ Query::$entries['site'] = function (): Site {
return App::instance()->site(); return App::instance()->site();
}; };
Query::$entries['t'] = function ( Query::$entries['t'] = function (
string $key, string $key,
string|array $fallback = null, string|array $fallback = null,

View File

@@ -51,6 +51,11 @@ class Segment
throw new BadMethodCallException($error); throw new BadMethodCallException($error);
} }
/**
* Parses a segment into the property/method name and its arguments
*
* @param int $position String position of the segment inside the full query
*/
public static function factory( public static function factory(
string $segment, string $segment,
int $position = 0 int $position = 0
@@ -69,6 +74,10 @@ class Segment
); );
} }
/**
* Automatically resolves the segment depending on the
* segment position and the type of the base
*/
public function resolve(mixed $base = null, array|object $data = []): mixed public function resolve(mixed $base = null, array|object $data = []): mixed
{ {
// resolve arguments to array // resolve arguments to array

View File

@@ -81,8 +81,9 @@ class Segments extends Collection
return null; return null;
} }
// for regular connectors, just skip // for regular connectors and optional chaining on non-null,
if ($segment === '.') { // just skip this connecting segment
if ($segment === '.' || $segment === '?.') {
continue; continue;
} }

View File

@@ -56,7 +56,8 @@ class Sane
// find a handler or alias // find a handler or alias
$alias = static::$aliases[$type] ?? null; $alias = static::$aliases[$type] ?? null;
$handler = static::$handlers[$type] ?? $handler =
static::$handlers[$type] ??
($alias ? static::$handlers[$alias] ?? null : null); ($alias ? static::$handlers[$alias] ?? null : null);
if (empty($handler) === false && class_exists($handler) === true) { if (empty($handler) === false && class_exists($handler) === true) {

View File

@@ -339,7 +339,11 @@ class Session
* @todo The $this->destroyed check gets flagged by Psalm for unknown reasons * @todo The $this->destroyed check gets flagged by Psalm for unknown reasons
* @psalm-suppress ParadoxicalCondition * @psalm-suppress ParadoxicalCondition
*/ */
if ($this->writeMode !== true || $this->tokenExpiry === null || $this->destroyed === true) { if (
$this->writeMode !== true ||
$this->tokenExpiry === null ||
$this->destroyed === true
) {
return; return;
} }
@@ -523,7 +527,11 @@ class Session
* @todo The $this->destroyed check gets flagged by Psalm for unknown reasons * @todo The $this->destroyed check gets flagged by Psalm for unknown reasons
* @psalm-suppress ParadoxicalCondition * @psalm-suppress ParadoxicalCondition
*/ */
if ($this->tokenExpiry === null || $this->destroyed === true || $this->writeMode === true) { if (
$this->tokenExpiry === null ||
$this->destroyed === true ||
$this->writeMode === true
) {
return; return;
} }

123
kirby/src/Template/Slot.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
namespace Kirby\Template;
use Kirby\Exception\LogicException;
/**
* The slot class catches all content
* between the beginning and the end of
* a slot. Slot content is then stored
* in the Slots collection.
*
* @package Kirby Template
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Slot
{
/**
* The captured slot content
* @internal
*/
public string|null $content;
/**
* The name that was declared during
* the definition of the slot
*/
protected string $name;
/**
* Keeps track of the slot state
*/
protected bool $open = false;
/**
* Creates a new slot
*/
public function __construct(string $name, string|null $content = null)
{
$this->name = $name;
$this->content = $content;
}
/**
* Renders the slot content or an empty string
* if the slot is empty.
*/
public function __toString(): string
{
return $this->render() ?? '';
}
/**
* Used in the slot helper
*/
public static function begin(string $name = 'default'): static|null
{
return Snippet::$current?->slot($name);
}
/**
* Closes a slot and catches all the content
* that has been printed since the slot has
* been opened
*/
public function close(): void
{
if ($this->open === false) {
throw new LogicException('The slot has not been opened');
}
$this->content = ob_get_clean();
$this->open = false;
}
/**
* Used in the endslot() helper
*/
public static function end(): void
{
Snippet::$current?->endslot();
}
/**
* Returns whether the slot is currently
* open and being buffered
*/
public function isOpen(): bool
{
return $this->open;
}
/**
* Returns the slot name
*/
public function name(): string
{
return $this->name;
}
/**
* Opens the slot and starts
* output buffering
*/
public function open(): void
{
$this->open = true;
// capture the output
ob_start();
}
/**
* Returns the slot content
*/
public function render(): string|null
{
return $this->content;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Kirby\Template;
use Countable;
/**
* The slots collection is simplifying
* slot access. Slots can be accessed with
* `$slots->heading()` and accessing a non-existing
* slot will simply return null.
*
* @package Kirby Template
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Slots implements Countable
{
/**
* Creates a new slots collection
*/
public function __construct(protected array $slots)
{
}
/**
* Magic getter for slots;
* e.g. `$slots->heading`
*/
public function __get(string $name): Slot|null
{
return $this->slots[$name] ?? null;
}
/**
* Magic getter method for slots;
* e.g. `$slots->heading()`
*/
public function __call(string $name, array $args): Slot|null
{
return $this->__get($name);
}
/**
* Counts the number of defined slots
*/
public function count(): int
{
return count($this->slots);
}
}

View File

@@ -0,0 +1,321 @@
<?php
namespace Kirby\Template;
use Kirby\Cms\App;
use Kirby\Cms\Helpers;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Toolkit\A;
use Kirby\Toolkit\Tpl;
/**
* The Snippet class includes shared code parts
* in templates and allows to pass data as well as to
* optionally pass content to various predefined slots.
*
* @package Kirby Template
* @author Bastian Allgeier <bastian@getkirby.com>,
* Nico Hoffmann <nico@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
*/
class Snippet extends Tpl
{
/**
* Cache for the currently active
* snippet. This is used to start
* and end slots within this snippet
* in the helper functions
* @internal
*/
public static self|null $current = null;
/**
* Contains all slots that are opened
* but not yet closed
*/
protected array $capture = [];
/**
* Associative array with variables that
* will be set inside the snippet
*/
protected array $data;
/**
* An empty dummy slots object used for snippets
* that were loaded without passing slots
*/
protected static Slots|null $dummySlots = null;
/**
* Full path to the PHP file of the snippet;
* can be `null` for "dummy" snippets that don't exist
*/
protected string|null $file;
/**
* Keeps track of the state of the snippet
*/
protected bool $open = false;
/**
* The parent snippet
*/
protected self|null $parent = null;
/**
* The collection of closed slots that will be used
* to pass down to the template for the snippet.
*/
protected array $slots = [];
/**
* Creates a new snippet
*/
public function __construct(string|null $file, array $data = [])
{
$this->file = $file;
$this->data = $data;
}
/**
* Creates and opens a new snippet. This can be used
* directly in a template or via the slots() helper
*/
public static function begin(string|null $file, array $data = []): static
{
$snippet = new static($file, $data);
return $snippet->open();
}
/**
* Closes the snippet and catches
* the default slot if no slots have been
* defined in between opening and closing.
*/
public function close(): static
{
// make sure that ending a snippet
// is only supported if the snippet has
// been started before
if ($this->open === false) {
throw new LogicException('The snippet has not been opened');
}
// create a default slot for the content
// that has been captured between start and end
if (empty($this->slots) === true) {
$this->slots['default'] = new Slot('default');
$this->slots['default']->content = ob_get_clean();
} else {
// swallow any "unslotted" content
// between start and end
ob_end_clean();
}
$this->open = false;
// switch back to the parent in nested
// snippet stacks
static::$current = $this->parent;
return $this;
}
/**
* Used in the endsnippet() helper
*/
public static function end(): void
{
echo static::$current?->render();
}
/**
* Closes the last openend slot
*/
public function endslot(): void
{
// take the last slot from the capture stack
$slot = array_pop($this->capture);
// capture the content and close the slot
$slot->close();
// add the slot to the scope
$this->slots[$slot->name()] = $slot;
}
/**
* Returns either an open snippet capturing slots
* or the template string for self-enclosed snippets
*/
public static function factory(
string|array $name,
array $data = [],
bool $slots = false
): static|string {
$file = static::file($name);
// for snippets with slots, make sure to open a new
// snippet and start capturing slots
if ($slots === true) {
return static::begin($file, $data);
}
// for snippets without slots, directly load and return
// the snippet's template file
return static::load($file, static::scope($data));
}
/**
* Absolute path to the file for
* the snippet/s taking snippets defined in plugins
* into account
*/
public static function file(string|array $name): string|null
{
$kirby = App::instance();
$root = static::root();
$names = A::wrap($name);
foreach ($names as $name) {
$name = (string)$name;
$file = $root . '/' . $name . '.php';
if (file_exists($file) === false) {
$file = $kirby->extensions('snippets')[$name] ?? null;
}
if ($file) {
break;
}
}
return $file;
}
/**
* Opens the snippet and starts output
* buffering to catch all slots in between
*/
public function open(): static
{
if (static::$current !== null) {
$this->parent = static::$current;
}
$this->open = true;
static::$current = $this;
ob_start();
return $this;
}
/**
* Returns the parent snippet if it exists
*/
public function parent(): static|null
{
return $this->parent;
}
/**
* Renders the snippet and passes the scope
* with all slots and data
*/
public function render(array $data = [], array $slots = []): string
{
// always make sure that the snippet
// is closed before it can be rendered
if ($this->open === true) {
$this->close();
}
// manually add slots
foreach ($slots as $slotName => $slotContent) {
$this->slots[$slotName] = new Slot($slotName, $slotContent);
}
// custom data overrides for the data that was passed to the snippet instance
$data = array_replace_recursive($this->data, $data);
return static::load($this->file, static::scope($data, $this->slots()));
}
/**
* Returns the root directory for all
* snippet templates
*/
public static function root(): string
{
return App::instance()->root('snippets');
}
/**
* Starts a new slot with the given name
*/
public function slot(string $name = 'default'): Slot
{
$slot = new Slot($name);
$slot->open();
// start a new slot
$this->capture[] = $slot;
return $slot;
}
/**
* Returns the slots collection
*/
public function slots(): Slots
{
return new Slots($this->slots);
}
/**
* Returns the data variables that get passed to a snippet
*
* @param \Kirby\Template\Slots|null $slots If null, an empty dummy object is used
*/
protected static function scope(array $data = [], Slots|null $slots = null): array
{
// initialize a dummy slots object and cache it for better performance
if ($slots === null) {
$slots = static::$dummySlots ??= new Slots([]);
}
$data = array_merge(App::instance()->data, $data);
// TODO 3.10: Replace the following code:
// if (
// array_key_exists('slot', $data) === true ||
// array_key_exists('slots', $data) === true
// ) {
// throw new InvalidArgumentException('Passing the $slot or $slots variables to snippets is not supported.');
// }
//
// return array_merge($data, [
// 'slot' => $slots->default,
// 'slots' => $slots,
// ]);
// @codeCoverageIgnoreStart
if (
array_key_exists('slot', $data) === true ||
array_key_exists('slots', $data) === true
) {
Helpers::deprecated('Passing the $slot or $slots variables to snippets is deprecated and will break in Kirby 3.10.');
}
// @codeCoverageIgnoreEnd
return array_merge([
'slot' => $slots->default,
'slots' => $slots,
], $data);
}
}

View File

@@ -1,8 +1,9 @@
<?php <?php
namespace Kirby\Cms; namespace Kirby\Template;
use Exception; use Exception;
use Kirby\Cms\App;
use Kirby\Filesystem\F; use Kirby\Filesystem\F;
use Kirby\Toolkit\Tpl; use Kirby\Toolkit\Tpl;
@@ -10,7 +11,7 @@ use Kirby\Toolkit\Tpl;
* Represents a Kirby template and takes care * Represents a Kirby template and takes care
* of loading the correct file. * of loading the correct file.
* *
* @package Kirby Cms * @package Kirby Template
* @author Bastian Allgeier <bastian@getkirby.com> * @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com * @link https://getkirby.com
* @copyright Bastian Allgeier * @copyright Bastian Allgeier
@@ -20,38 +21,26 @@ class Template
{ {
/** /**
* Global template data * Global template data
*
* @var array
*/ */
public static $data = []; public static array $data = [];
/**
* The name of the template
*
* @var string
*/
protected $name;
/**
* Template type (html, json, etc.)
*
* @var string
*/
protected $type;
/** /**
* Default template type if no specific type is set * Default template type if no specific type is set
*
* @var string
*/ */
protected $defaultType; protected string $defaultType;
/**
* The name of the template
*/
protected string $name;
/**
* Template type (html, json, etc.)
*/
protected string $type;
/** /**
* Creates a new template object * Creates a new template object
*
* @param string $name
* @param string $type
* @param string $defaultType
*/ */
public function __construct(string $name, string $type = 'html', string $defaultType = 'html') public function __construct(string $name, string $type = 'html', string $defaultType = 'html')
{ {
@@ -63,18 +52,22 @@ class Template
/** /**
* Converts the object to a simple string * Converts the object to a simple string
* This is used in template filters for example * This is used in template filters for example
*
* @return string
*/ */
public function __toString(): string public function __toString(): string
{ {
return $this->name; return $this->name;
} }
/**
* Returns the default template type
*/
public function defaultType(): string
{
return $this->defaultType;
}
/** /**
* Checks if the template exists * Checks if the template exists
*
* @return bool
*/ */
public function exists(): bool public function exists(): bool
{ {
@@ -87,75 +80,61 @@ class Template
/** /**
* Returns the expected template file extension * Returns the expected template file extension
*
* @return string
*/ */
public function extension(): string public function extension(): string
{ {
return 'php'; return 'php';
} }
/**
* Returns the default template type
*
* @return string
*/
public function defaultType(): string
{
return $this->defaultType;
}
/**
* Returns the place where templates are located
* in the site folder and and can be found in extensions
*
* @return string
*/
public function store(): string
{
return 'templates';
}
/** /**
* Detects the location of the template file * Detects the location of the template file
* if it exists. * if it exists.
*
* @return string|null
*/ */
public function file(): string|null public function file(): string|null
{ {
$name = $this->name();
$extension = $this->extension();
$store = $this->store();
$root = $this->root();
if ($this->hasDefaultType() === true) { if ($this->hasDefaultType() === true) {
try { try {
// Try the default template in the default template directory. // Try the default template in the default template directory.
return F::realpath($this->root() . '/' . $this->name() . '.' . $this->extension(), $this->root()); return F::realpath($root . '/' . $name . '.' . $extension, $root);
} catch (Exception) { } catch (Exception) {
// ignore errors, continue searching // ignore errors, continue searching
} }
// Look for the default template provided by an extension. // Look for the default template provided by an extension.
$path = App::instance()->extension($this->store(), $this->name()); $path = App::instance()->extension($store, $name);
if ($path !== null) { if ($path !== null) {
return $path; return $path;
} }
} }
$name = $this->name() . '.' . $this->type(); $name .= '.' . $this->type();
try { try {
// Try the template with type extension in the default template directory. // Try the template with type extension in the default template directory.
return F::realpath($this->root() . '/' . $name . '.' . $this->extension(), $this->root()); return F::realpath($root . '/' . $name . '.' . $extension, $root);
} catch (Exception) { } catch (Exception) {
// Look for the template with type extension provided by an extension. // Look for the template with type extension provided by an extension.
// This might be null if the template does not exist. // This might be null if the template does not exist.
return App::instance()->extension($this->store(), $name); return App::instance()->extension($store, $name);
} }
} }
/**
* Checks if the template uses the default type
*/
public function hasDefaultType(): bool
{
return $this->type() === $this->defaultType();
}
/** /**
* Returns the template name * Returns the template name
*
* @return string
*/ */
public function name(): string public function name(): string
{ {
@@ -163,43 +142,67 @@ class Template
} }
/** /**
* @param array $data * Renders the template with the given template data
* @return string
*/ */
public function render(array $data = []): string public function render(array $data = []): string
{ {
return Tpl::load($this->file(), $data); // if the template is rendered inside a snippet,
// we need to keep the "outside" snippet object
// to compare it later
$snippet = Snippet::$current;
// load the template
$template = Tpl::load($this->file(), $data);
// if last `endsnippet()` inside the current template
// has been omitted (= snippet was used as layout snippet),
// `Snippet::$current` will point to a snippet that was
// opened inside the template; if that snippet is the direct
// child of the snippet that was open before the template was
// rendered (which could be `null` if no snippet was open),
// take the buffer output from the template as default slot
// and render the snippet as final template output
if (
Snippet::$current === null ||
Snippet::$current->parent() !== $snippet
) {
return $template;
}
// no slots have been defined, but the template code
// should be used as default slot
if (Snippet::$current->slots()->count() === 0) {
return Snippet::$current->render($data, [
'default' => $template
]);
}
// let the snippet close and render natively
return Snippet::$current->render($data);
} }
/** /**
* Returns the root to the templates directory * Returns the root to the templates directory
*
* @return string
*/ */
public function root(): string public function root(): string
{ {
return App::instance()->root($this->store()); return App::instance()->root($this->store());
} }
/**
* Returns the place where templates are located
* in the site folder and and can be found in extensions
*/
public function store(): string
{
return 'templates';
}
/** /**
* Returns the template type * Returns the template type
*
* @return string
*/ */
public function type(): string public function type(): string
{ {
return $this->type; return $this->type;
} }
/**
* Checks if the template uses the default type
*
* @return bool
*/
public function hasDefaultType(): bool
{
$type = $this->type();
return $type === null || $type === $this->defaultType();
}
} }

View File

@@ -148,6 +148,7 @@ class KirbyTag
return $this->kirby()->file($path, null, true); return $this->kirby()->file($path, null, true);
} }
/** /**
* Returns the current Kirby instance * Returns the current Kirby instance
*/ */

View File

@@ -25,6 +25,9 @@ class KirbyTags
array $data = [], array $data = [],
array $options = [] array $options = []
): string { ): string {
// make sure $text is a string
$text ??= '';
$regex = '! $regex = '!
(?=[^\]]) # positive lookahead that matches a group after the main expression without including ] in the result (?=[^\]]) # positive lookahead that matches a group after the main expression without including ] in the result
(?=\([a-z0-9_-]+:) # positive lookahead that requires starts with ( and lowercase ASCII letters, digits, underscores or hyphens followed with : immediately to the right of the current location (?=\([a-z0-9_-]+:) # positive lookahead that requires starts with ( and lowercase ASCII letters, digits, underscores or hyphens followed with : immediately to the right of the current location
@@ -40,7 +43,10 @@ class KirbyTags
return KirbyTag::parse($match[0], $data, $options)->render(); return KirbyTag::parse($match[0], $data, $options)->render();
} catch (InvalidArgumentException $e) { } catch (InvalidArgumentException $e) {
// stay silent in production and ignore non-existing tags // stay silent in production and ignore non-existing tags
if ($debug !== true || Str::startsWith($e->getMessage(), 'Undefined tag type:') === true) { if (
$debug !== true ||
Str::startsWith($e->getMessage(), 'Undefined tag type:') === true
) {
return $match[0]; return $match[0];
} }
@@ -52,6 +58,6 @@ class KirbyTags
return $match[0]; return $match[0];
} }
}, $text ?? ''); }, $text);
} }
} }

View File

@@ -53,11 +53,10 @@ class Markdown
*/ */
public function parse(string|null $text = null, bool $inline = false): string public function parse(string|null $text = null, bool $inline = false): string
{ {
if ($this->options['extra'] === true) { $parser = match ($this->options['extra']) {
$parser = new ParsedownExtra(); true => new ParsedownExtra(),
} else { default => new Parsedown()
$parser = new Parsedown(); };
}
$parser->setBreaksEnabled($this->options['breaks']); $parser->setBreaksEnabled($this->options['breaks']);
$parser->setSafeMode($this->options['safe']); $parser->setSafeMode($this->options['safe']);

View File

@@ -110,7 +110,8 @@ class SmartyPants
public function parse(string|null $text = null): string public function parse(string|null $text = null): string
{ {
// prepare the text // prepare the text
$text = str_replace('&quot;', '"', $text ?? ''); $text ??= '';
$text = str_replace('&quot;', '"', $text);
// parse the text // parse the text
return $this->parser->transform($text); return $this->parser->transform($text);

View File

@@ -114,9 +114,16 @@ class A
// the element needs to exist and also needs to be an array; otherwise // the element needs to exist and also needs to be an array; otherwise
// we cannot find the remaining keys within it (invalid array structure) // we cannot find the remaining keys within it (invalid array structure)
if (isset($array[$currentKey]) === true && is_array($array[$currentKey]) === true) { if (
isset($array[$currentKey]) === true &&
is_array($array[$currentKey]) === true
) {
// $keys only holds the remaining keys that have not been shifted off yet // $keys only holds the remaining keys that have not been shifted off yet
return static::get($array[$currentKey], implode('.', $keys), $default); return static::get(
$array[$currentKey],
implode('.', $keys),
$default
);
} }
} }
@@ -127,7 +134,11 @@ class A
// if the input array uses a completely nested structure, // if the input array uses a completely nested structure,
// recursively progress layer by layer // recursively progress layer by layer
if (is_array($array[$firstKey]) === true) { if (is_array($array[$firstKey]) === true) {
return static::get($array[$firstKey], implode('.', $keys), $default); return static::get(
$array[$firstKey],
implode('.', $keys),
$default
);
} }
// the $firstKey element was found, but isn't an array, so we cannot // the $firstKey element was found, but isn't an array, so we cannot
@@ -146,6 +157,7 @@ class A
if (is_string($value) === true) { if (is_string($value) === true) {
return $value; return $value;
} }
return implode($separator, $value); return implode($separator, $value);
} }
@@ -251,6 +263,7 @@ class A
public static function pluck(array $array, string $key): array public static function pluck(array $array, string $key): array
{ {
$output = []; $output = [];
foreach ($array as $a) { foreach ($array as $a) {
if (isset($a[$key]) === true) { if (isset($a[$key]) === true) {
$output[] = $a[$key]; $output[] = $a[$key];
@@ -396,12 +409,12 @@ class A
*/ */
public static function fill(array $array, int $limit, $fill = 'placeholder'): array public static function fill(array $array, int $limit, $fill = 'placeholder'): array
{ {
if (count($array) < $limit) {
$diff = $limit - count($array); $diff = $limit - count($array);
for ($x = 0; $x < $diff; $x++) { for ($x = 0; $x < $diff; $x++) {
$array[] = $fill; $array[] = $fill;
} }
}
return $array; return $array;
} }
@@ -467,13 +480,7 @@ class A
*/ */
public static function missing(array $array, array $required = []): array public static function missing(array $array, array $required = []): array
{ {
$missing = []; return array_values(array_diff($required, array_keys($array)));
foreach ($required as $r) {
if (isset($array[$r]) === false) {
$missing[] = $r;
}
}
return $missing;
} }
/** /**

View File

@@ -1464,7 +1464,8 @@ Collection::$filters['date <='] = [
*/ */
Collection::$filters['date between'] = Collection::$filters['date ..'] = [ Collection::$filters['date between'] = Collection::$filters['date ..'] = [
'validator' => function ($value, $test) { 'validator' => function ($value, $test) {
return V::date($value, '>=', $test[0]) && return
V::date($value, '>=', $test[0]) &&
V::date($value, '<=', $test[1]); V::date($value, '<=', $test[1]);
} }
]; ];

Some files were not shown because too many files have changed in this diff Show More