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
+1 -1
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
+1 -1
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');
} }
+8 -7
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"
} }
} }
+34 -32
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": "*",
+5
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',
+2 -1
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,
+7 -3
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)
); );
} }
], ],
+11
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;
}
} }
], ],
]; ];
+7 -2
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
); );
} }
+8 -3
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' => [
+44 -46
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);
+5 -2
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');
+13
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());
}
] ]
]; ];
+1 -1
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]
+39 -8
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);
} }
} }
+1 -1
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
@@ -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;
+5 -1
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",
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -11
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -2
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) {
+1 -5
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) {
+12 -7
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 = [];
+1 -1
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
+1 -1
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
+2 -2
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);
} }
+1 -2
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
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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
+1 -1
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
+2 -1
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)
{ {
+15 -22
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
+2 -1
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);
+1 -4
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 {
@@ -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);
}
}
+22 -22
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();
+6 -2
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;
+2 -1
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;
} }
+1 -1
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)
{ {
+15 -6
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');
} }
+3 -2
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);
} }
/** /**
+4 -1
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;
} }
+2 -1
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'] ?? '') === ''
+1 -5
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());
} }
+1 -3
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) {
+10 -10
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()
{ {
+2 -4
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) {
+1 -3
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;
+47 -101
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();
}
} }
+1 -3
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
+20 -17
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']);
} }
+9 -10
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);
} }
} }
+15 -13
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',
+2 -1
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) {
+23 -8
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
+10 -3
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;
} }
+8 -1
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;
} }
+4 -1
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) {
+1 -3
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) {
+1 -1
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);
+6 -2
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;
} }
+5 -2
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);
+2 -6
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'])) {
+1
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
{ {
+2 -2
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);
+5 -2
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;
} }
+23 -5
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);
}
} }
} }
+2 -1
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;
} }
+6
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);
} }
+3 -3
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];
+3 -1
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 ||
+33 -6
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);
+9 -1
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;
} }
+10 -5
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');
}); });
+4 -2
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';
} }
+3 -5
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;
} }
+1 -4
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);
} }
+3 -1
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
{ {
+12 -7
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);
} }
+4 -3
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;
/** /**
+3
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
-1
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,
+9
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
+3 -2
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;
} }
+2 -1
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) {
+10 -2
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
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;
}
}
+53
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);
}
}
+321
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);
}
}
@@ -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();
}
} }
+1
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
*/ */
+8 -2
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);
} }
} }
+4 -5
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']);
+2 -1
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);
+19 -12
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;
} }
/** /**
+2 -1
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