From a1f070163049e1ebc78ac68c9d8ae4e57d6a16cc Mon Sep 17 00:00:00 2001
From: isUnknown
Date: Tue, 23 Sep 2025 08:15:07 +0200
Subject: [PATCH] composer update
---
package-lock.json | 626 ++++++++++++------
package.json | 2 +-
public/composer.lock | 63 +-
public/kirby/cacert.pem | 4 +-
public/kirby/composer.json | 11 +-
public/kirby/composer.lock | 62 +-
public/kirby/config/areas/account/dialogs.php | 68 +-
public/kirby/config/areas/account/drawers.php | 11 +-
.../kirby/config/areas/account/dropdowns.php | 8 +-
public/kirby/config/areas/lab/drawers.php | 4 +-
public/kirby/config/areas/site/dialogs.php | 83 +--
public/kirby/config/areas/site/drawers.php | 23 +-
public/kirby/config/areas/site/dropdowns.php | 4 +-
public/kirby/config/areas/system/dialogs.php | 22 +
public/kirby/config/areas/users/dialogs.php | 45 +-
public/kirby/config/areas/users/drawers.php | 12 +-
public/kirby/config/areas/users/dropdowns.php | 4 +-
public/kirby/config/areas/users/views.php | 31 +-
public/kirby/config/components.php | 5 +-
public/kirby/config/fields/object.php | 2 +-
public/kirby/config/fields/structure.php | 9 +-
public/kirby/config/fields/users.php | 8 +-
public/kirby/config/sections/files.php | 126 +---
.../kirby/config/sections/mixins/search.php | 6 +-
public/kirby/config/sections/pages.php | 122 +---
public/kirby/config/sections/stats.php | 60 +-
public/kirby/config/tags.php | 7 +-
public/kirby/i18n/translations/en.json | 2 +
public/kirby/i18n/translations/ko.json | 106 +--
public/kirby/i18n/translations/nl.json | 10 +-
public/kirby/src/Cms/App.php | 57 +-
public/kirby/src/Cms/AppTranslations.php | 6 +
public/kirby/src/Cms/Core.php | 2 +
public/kirby/src/Cms/File.php | 3 +-
public/kirby/src/Cms/LanguageVariable.php | 9 +
public/kirby/src/Cms/License.php | 38 +-
public/kirby/src/Cms/Page.php | 3 +-
public/kirby/src/Cms/PageActions.php | 3 +
public/kirby/src/Cms/Responder.php | 10 +-
public/kirby/src/Cms/Site.php | 2 +
public/kirby/src/Cms/User.php | 1 +
public/kirby/src/Filesystem/Mime.php | 2 +-
public/kirby/src/Form/Field/StatsField.php | 74 +++
public/kirby/src/Form/FieldClass.php | 127 +---
public/kirby/src/Form/Mixin/After.php | 21 +
public/kirby/src/Form/Mixin/Autofocus.php | 21 +
public/kirby/src/Form/Mixin/Before.php | 21 +
public/kirby/src/Form/Mixin/EmptyState.php | 3 +
public/kirby/src/Form/Mixin/Help.php | 34 +
public/kirby/src/Form/Mixin/Icon.php | 28 +
public/kirby/src/Form/Mixin/Label.php | 32 +
public/kirby/src/Form/Mixin/Max.php | 3 +
public/kirby/src/Form/Mixin/Min.php | 3 +
public/kirby/src/Form/Mixin/Placeholder.php | 30 +
public/kirby/src/Form/Mixin/Translatable.php | 9 +-
public/kirby/src/Form/Mixin/Validation.php | 6 +-
public/kirby/src/Form/Mixin/Value.php | 7 +
public/kirby/src/Form/Mixin/When.php | 11 +-
public/kirby/src/Form/Mixin/Width.php | 29 +
public/kirby/src/Http/Cookie.php | 3 +-
public/kirby/src/Http/Environment.php | 2 +-
public/kirby/src/Http/Header.php | 2 +-
public/kirby/src/Http/Params.php | 49 +-
public/kirby/src/Http/Query.php | 21 +-
public/kirby/src/Http/Remote.php | 18 +-
public/kirby/src/Http/Request.php | 42 +-
public/kirby/src/Http/Request/Body.php | 17 +-
public/kirby/src/Http/Request/Data.php | 2 +
public/kirby/src/Http/Request/Files.php | 2 +-
public/kirby/src/Http/Request/Query.php | 6 +-
public/kirby/src/Http/Response.php | 20 +-
public/kirby/src/Http/Route.php | 26 +-
public/kirby/src/Http/Router.php | 11 +-
public/kirby/src/Http/Uri.php | 39 +-
public/kirby/src/Http/Url.php | 16 +-
public/kirby/src/Http/Visitor.php | 12 +-
public/kirby/src/Image/Darkroom.php | 26 +-
public/kirby/src/Image/Darkroom/GdLib.php | 2 +-
.../kirby/src/Image/Darkroom/ImageMagick.php | 5 +-
public/kirby/src/Image/Darkroom/Imagick.php | 292 ++++++++
public/kirby/src/Image/Exif.php | 7 +-
.../src/Panel/Collector/FilesCollector.php | 73 ++
.../src/Panel/Collector/ModelsCollector.php | 130 ++++
.../src/Panel/Collector/PagesCollector.php | 85 +++
.../src/Panel/Collector/UsersCollector.php | 62 ++
public/kirby/src/Panel/Controller/Search.php | 28 +-
public/kirby/src/Panel/Dialog.php | 14 +
public/kirby/src/Panel/File.php | 23 +-
public/kirby/src/Panel/Model.php | 20 +-
public/kirby/src/Panel/Page.php | 14 +-
public/kirby/src/Panel/PageCreateDialog.php | 46 +-
public/kirby/src/Panel/Ui/Component.php | 5 +-
public/kirby/src/Panel/Ui/Item/FileItem.php | 74 +++
public/kirby/src/Panel/Ui/Item/ModelItem.php | 74 +++
public/kirby/src/Panel/Ui/Item/PageItem.php | 74 +++
public/kirby/src/Panel/Ui/Item/UserItem.php | 38 ++
public/kirby/src/Panel/Ui/Stat.php | 140 ++++
public/kirby/src/Panel/Ui/Stats.php | 83 +++
public/kirby/src/Panel/Ui/Upload.php | 62 ++
public/kirby/src/Panel/User.php | 12 +-
public/kirby/src/Panel/View.php | 1 +
.../kirby/src/Query/AST/ArgumentListNode.php | 37 ++
public/kirby/src/Query/AST/ArithmeticNode.php | 34 +
public/kirby/src/Query/AST/ArrayListNode.php | 37 ++
public/kirby/src/Query/AST/ClosureNode.php | 33 +
public/kirby/src/Query/AST/CoalesceNode.php | 33 +
public/kirby/src/Query/AST/ComparisonNode.php | 34 +
.../src/Query/AST/GlobalFunctionNode.php | 33 +
public/kirby/src/Query/AST/LiteralNode.php | 29 +
public/kirby/src/Query/AST/LogicalNode.php | 34 +
.../kirby/src/Query/AST/MemberAccessNode.php | 37 ++
public/kirby/src/Query/AST/Node.php | 23 +
public/kirby/src/Query/AST/TernaryNode.php | 36 +
public/kirby/src/Query/AST/VariableNode.php | 29 +
public/kirby/src/Query/Argument.php | 2 +
public/kirby/src/Query/Arguments.php | 2 +
public/kirby/src/Query/Expression.php | 2 +
public/kirby/src/Query/Parser/Parser.php | 476 +++++++++++++
public/kirby/src/Query/Parser/Token.php | 30 +
public/kirby/src/Query/Parser/TokenType.php | 61 ++
public/kirby/src/Query/Parser/Tokenizer.php | 256 +++++++
public/kirby/src/Query/Query.php | 52 +-
.../kirby/src/Query/Runners/DefaultRunner.php | 69 ++
public/kirby/src/Query/Runners/Runner.php | 43 ++
public/kirby/src/Query/Runners/Scope.php | 94 +++
public/kirby/src/Query/Segment.php | 2 +
public/kirby/src/Query/Segments.php | 2 +
.../src/Query/Visitors/DefaultVisitor.php | 188 ++++++
public/kirby/src/Query/Visitors/Visitor.php | 46 ++
public/kirby/src/Session/AutoSession.php | 7 +-
public/kirby/src/Session/Session.php | 5 +-
public/kirby/src/Session/Sessions.php | 17 +-
public/kirby/src/Template/Snippet.php | 43 +-
public/kirby/src/Toolkit/Collection.php | 13 +-
public/kirby/src/Toolkit/Str.php | 6 +-
public/kirby/src/Toolkit/V.php | 2 +-
public/kirby/src/Uuid/FileUuid.php | 10 +-
public/kirby/src/Uuid/HasUuids.php | 2 +-
public/kirby/src/Uuid/PageUuid.php | 12 +-
public/kirby/src/Uuid/Uuid.php | 33 +-
public/kirby/views/php.php | 2 +-
public/media/index.html | 0
142 files changed, 4530 insertions(+), 1195 deletions(-)
create mode 100644 public/kirby/src/Form/Field/StatsField.php
create mode 100644 public/kirby/src/Form/Mixin/After.php
create mode 100644 public/kirby/src/Form/Mixin/Autofocus.php
create mode 100644 public/kirby/src/Form/Mixin/Before.php
create mode 100644 public/kirby/src/Form/Mixin/Help.php
create mode 100644 public/kirby/src/Form/Mixin/Icon.php
create mode 100644 public/kirby/src/Form/Mixin/Label.php
create mode 100644 public/kirby/src/Form/Mixin/Placeholder.php
create mode 100644 public/kirby/src/Form/Mixin/Width.php
create mode 100644 public/kirby/src/Image/Darkroom/Imagick.php
create mode 100644 public/kirby/src/Panel/Collector/FilesCollector.php
create mode 100644 public/kirby/src/Panel/Collector/ModelsCollector.php
create mode 100644 public/kirby/src/Panel/Collector/PagesCollector.php
create mode 100644 public/kirby/src/Panel/Collector/UsersCollector.php
create mode 100644 public/kirby/src/Panel/Ui/Item/FileItem.php
create mode 100644 public/kirby/src/Panel/Ui/Item/ModelItem.php
create mode 100644 public/kirby/src/Panel/Ui/Item/PageItem.php
create mode 100644 public/kirby/src/Panel/Ui/Item/UserItem.php
create mode 100644 public/kirby/src/Panel/Ui/Stat.php
create mode 100644 public/kirby/src/Panel/Ui/Stats.php
create mode 100644 public/kirby/src/Panel/Ui/Upload.php
create mode 100644 public/kirby/src/Query/AST/ArgumentListNode.php
create mode 100644 public/kirby/src/Query/AST/ArithmeticNode.php
create mode 100644 public/kirby/src/Query/AST/ArrayListNode.php
create mode 100644 public/kirby/src/Query/AST/ClosureNode.php
create mode 100644 public/kirby/src/Query/AST/CoalesceNode.php
create mode 100644 public/kirby/src/Query/AST/ComparisonNode.php
create mode 100644 public/kirby/src/Query/AST/GlobalFunctionNode.php
create mode 100644 public/kirby/src/Query/AST/LiteralNode.php
create mode 100644 public/kirby/src/Query/AST/LogicalNode.php
create mode 100644 public/kirby/src/Query/AST/MemberAccessNode.php
create mode 100644 public/kirby/src/Query/AST/Node.php
create mode 100644 public/kirby/src/Query/AST/TernaryNode.php
create mode 100644 public/kirby/src/Query/AST/VariableNode.php
create mode 100644 public/kirby/src/Query/Parser/Parser.php
create mode 100644 public/kirby/src/Query/Parser/Token.php
create mode 100644 public/kirby/src/Query/Parser/TokenType.php
create mode 100644 public/kirby/src/Query/Parser/Tokenizer.php
create mode 100644 public/kirby/src/Query/Runners/DefaultRunner.php
create mode 100644 public/kirby/src/Query/Runners/Runner.php
create mode 100644 public/kirby/src/Query/Runners/Scope.php
create mode 100644 public/kirby/src/Query/Visitors/DefaultVisitor.php
create mode 100644 public/kirby/src/Query/Visitors/Visitor.php
delete mode 100644 public/media/index.html
diff --git a/package-lock.json b/package-lock.json
index ec3fbd9..76f5425 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.3",
- "vite": "^5.4.3"
+ "vite": "^7.1.6"
}
},
"node_modules/@babel/helper-string-parser": {
@@ -73,348 +73,419 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
+ "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
"cpu": [
"ppc64"
],
+ "license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
+ "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
"cpu": [
"arm"
],
+ "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
+ "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
+ "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
+ "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
+ "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
+ "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
+ "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
"cpu": [
"arm"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
+ "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
+ "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
"cpu": [
"ia32"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
+ "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
"cpu": [
"loong64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
+ "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
"cpu": [
"mips64el"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
+ "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
"cpu": [
"ppc64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
+ "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
"cpu": [
"riscv64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
+ "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
"cpu": [
"s390x"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
+ "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
"cpu": [
- "x64"
+ "arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
"cpu": [
"x64"
],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
+ "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
+ "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
"cpu": [
"x64"
],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
+ "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
+ "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
+ "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
"cpu": [
"arm64"
],
+ "license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
+ "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
"cpu": [
"ia32"
],
+ "license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
+ "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
"cpu": [
"x64"
],
+ "license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@floating-ui/core": {
@@ -739,9 +810,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
- "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.2.tgz",
+ "integrity": "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A==",
"cpu": [
"arm"
],
@@ -752,9 +823,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
- "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.2.tgz",
+ "integrity": "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g==",
"cpu": [
"arm64"
],
@@ -765,9 +836,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
- "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.2.tgz",
+ "integrity": "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q==",
"cpu": [
"arm64"
],
@@ -778,9 +849,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
- "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.2.tgz",
+ "integrity": "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A==",
"cpu": [
"x64"
],
@@ -790,10 +861,36 @@
"darwin"
]
},
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.2.tgz",
+ "integrity": "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.2.tgz",
+ "integrity": "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
- "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.2.tgz",
+ "integrity": "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w==",
"cpu": [
"arm"
],
@@ -804,9 +901,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
- "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.2.tgz",
+ "integrity": "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw==",
"cpu": [
"arm"
],
@@ -817,9 +914,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
- "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.2.tgz",
+ "integrity": "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg==",
"cpu": [
"arm64"
],
@@ -830,9 +927,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
- "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.2.tgz",
+ "integrity": "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ==",
"cpu": [
"arm64"
],
@@ -842,10 +939,23 @@
"linux"
]
},
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
- "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.50.2.tgz",
+ "integrity": "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.2.tgz",
+ "integrity": "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag==",
"cpu": [
"ppc64"
],
@@ -856,9 +966,22 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
- "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.2.tgz",
+ "integrity": "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.2.tgz",
+ "integrity": "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw==",
"cpu": [
"riscv64"
],
@@ -869,9 +992,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
- "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.2.tgz",
+ "integrity": "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w==",
"cpu": [
"s390x"
],
@@ -882,9 +1005,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
- "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz",
+ "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==",
"cpu": [
"x64"
],
@@ -895,9 +1018,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
- "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.2.tgz",
+ "integrity": "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw==",
"cpu": [
"x64"
],
@@ -907,10 +1030,23 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.2.tgz",
+ "integrity": "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
- "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.2.tgz",
+ "integrity": "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA==",
"cpu": [
"arm64"
],
@@ -921,9 +1057,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
- "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.2.tgz",
+ "integrity": "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA==",
"cpu": [
"ia32"
],
@@ -934,9 +1070,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
- "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.2.tgz",
+ "integrity": "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA==",
"cpu": [
"x64"
],
@@ -982,9 +1118,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
- "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
@@ -1220,40 +1356,44 @@
}
},
"node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "version": "0.25.10",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
+ "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
"hasInstallScript": true,
+ "license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
+ "@esbuild/aix-ppc64": "0.25.10",
+ "@esbuild/android-arm": "0.25.10",
+ "@esbuild/android-arm64": "0.25.10",
+ "@esbuild/android-x64": "0.25.10",
+ "@esbuild/darwin-arm64": "0.25.10",
+ "@esbuild/darwin-x64": "0.25.10",
+ "@esbuild/freebsd-arm64": "0.25.10",
+ "@esbuild/freebsd-x64": "0.25.10",
+ "@esbuild/linux-arm": "0.25.10",
+ "@esbuild/linux-arm64": "0.25.10",
+ "@esbuild/linux-ia32": "0.25.10",
+ "@esbuild/linux-loong64": "0.25.10",
+ "@esbuild/linux-mips64el": "0.25.10",
+ "@esbuild/linux-ppc64": "0.25.10",
+ "@esbuild/linux-riscv64": "0.25.10",
+ "@esbuild/linux-s390x": "0.25.10",
+ "@esbuild/linux-x64": "0.25.10",
+ "@esbuild/netbsd-arm64": "0.25.10",
+ "@esbuild/netbsd-x64": "0.25.10",
+ "@esbuild/openbsd-arm64": "0.25.10",
+ "@esbuild/openbsd-x64": "0.25.10",
+ "@esbuild/openharmony-arm64": "0.25.10",
+ "@esbuild/sunos-x64": "0.25.10",
+ "@esbuild/win32-arm64": "0.25.10",
+ "@esbuild/win32-ia32": "0.25.10",
+ "@esbuild/win32-x64": "0.25.10"
}
},
"node_modules/estree-walker": {
@@ -1262,11 +1402,29 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -1331,6 +1489,18 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/pinia": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",
@@ -1482,12 +1652,12 @@
}
},
"node_modules/rollup": {
- "version": "4.24.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
- "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
+ "version": "4.50.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz",
+ "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==",
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.6"
+ "@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -1497,22 +1667,27 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.24.0",
- "@rollup/rollup-android-arm64": "4.24.0",
- "@rollup/rollup-darwin-arm64": "4.24.0",
- "@rollup/rollup-darwin-x64": "4.24.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.24.0",
- "@rollup/rollup-linux-arm64-gnu": "4.24.0",
- "@rollup/rollup-linux-arm64-musl": "4.24.0",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.24.0",
- "@rollup/rollup-linux-s390x-gnu": "4.24.0",
- "@rollup/rollup-linux-x64-gnu": "4.24.0",
- "@rollup/rollup-linux-x64-musl": "4.24.0",
- "@rollup/rollup-win32-arm64-msvc": "4.24.0",
- "@rollup/rollup-win32-ia32-msvc": "4.24.0",
- "@rollup/rollup-win32-x64-msvc": "4.24.0",
+ "@rollup/rollup-android-arm-eabi": "4.50.2",
+ "@rollup/rollup-android-arm64": "4.50.2",
+ "@rollup/rollup-darwin-arm64": "4.50.2",
+ "@rollup/rollup-darwin-x64": "4.50.2",
+ "@rollup/rollup-freebsd-arm64": "4.50.2",
+ "@rollup/rollup-freebsd-x64": "4.50.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.50.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.50.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.50.2",
+ "@rollup/rollup-linux-arm64-musl": "4.50.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.50.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.50.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.50.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.50.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.50.2",
+ "@rollup/rollup-linux-x64-gnu": "4.50.2",
+ "@rollup/rollup-linux-x64-musl": "4.50.2",
+ "@rollup/rollup-openharmony-arm64": "4.50.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.50.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.50.2",
+ "@rollup/rollup-win32-x64-msvc": "4.50.2",
"fsevents": "~2.3.2"
}
},
@@ -1537,6 +1712,22 @@
"resolved": "https://registry.npmjs.org/three/-/three-0.168.0.tgz",
"integrity": "sha512-6m6jXtDwMJEK/GGMbAOTSAmxNdzKvvBzgd7q8bE/7Tr6m7PaBh5kKLrN7faWtlglXbzj7sVba48Idwx+NRsZXw=="
},
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -1549,20 +1740,23 @@
"integrity": "sha512-38JRbJ4Fj94VmnC7G/J/5n5SC7Ab46OM5iNtSstB/ko3l1b5g7ALt4qzHFgGciFkyiRNtDXtLNb+VsxtMSE77A=="
},
"node_modules/vite": {
- "version": "5.4.9",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz",
- "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==",
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.6.tgz",
+ "integrity": "sha512-SRYIB8t/isTwNn8vMB3MR6E+EQZM/WG1aKmmIUCfDXfVvKfc20ZpamngWHKzAmmu9ppsgxsg4b2I7c90JZudIQ==",
"license": "MIT",
"dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^18.0.0 || >=20.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -1571,19 +1765,25 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
- "less": "*",
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
"lightningcss": "^1.21.0",
- "sass": "*",
- "sass-embedded": "*",
- "stylus": "*",
- "sugarss": "*",
- "terser": "^5.4.0"
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
+ "jiti": {
+ "optional": true
+ },
"less": {
"optional": true
},
@@ -1604,6 +1804,12 @@
},
"terser": {
"optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
}
}
},
diff --git a/package.json b/package.json
index 99976e0..6a387f9 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,6 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.3",
- "vite": "^5.4.3"
+ "vite": "^7.1.6"
}
}
diff --git a/public/composer.lock b/public/composer.lock
index 2d2eb27..08f41ec 100644
--- a/public/composer.lock
+++ b/public/composer.lock
@@ -120,16 +120,16 @@
},
{
"name": "composer/semver",
- "version": "3.4.3",
+ "version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
- "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
+ "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
@@ -181,7 +181,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
- "source": "https://github.com/composer/semver/tree/3.4.3"
+ "source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
@@ -191,13 +191,9 @@
{
"url": "https://github.com/composer",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
}
],
- "time": "2024-09-19T14:15:21+00:00"
+ "time": "2025-08-20T19:15:30+00:00"
},
{
"name": "filp/whoops",
@@ -272,22 +268,22 @@
},
{
"name": "getkirby/cms",
- "version": "5.0.4",
+ "version": "5.1.1",
"source": {
"type": "git",
"url": "https://github.com/getkirby/kirby.git",
- "reference": "c9708e5c648fbc3ee381114ceae8876d4ca4f9a1"
+ "reference": "fb11f5e3ec422e948fb1a52f16988335bb3489b4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/getkirby/kirby/zipball/c9708e5c648fbc3ee381114ceae8876d4ca4f9a1",
- "reference": "c9708e5c648fbc3ee381114ceae8876d4ca4f9a1",
+ "url": "https://api.github.com/repos/getkirby/kirby/zipball/fb11f5e3ec422e948fb1a52f16988335bb3489b4",
+ "reference": "fb11f5e3ec422e948fb1a52f16988335bb3489b4",
"shasum": ""
},
"require": {
"christian-riesen/base32": "1.6.0",
"claviska/simpleimage": "4.2.1",
- "composer/semver": "3.4.3",
+ "composer/semver": "3.4.4",
"ext-ctype": "*",
"ext-curl": "*",
"ext-dom": "*",
@@ -305,9 +301,9 @@
"michelf/php-smartypants": "1.8.1",
"php": "~8.2.0 || ~8.3.0 || ~8.4.0",
"phpmailer/phpmailer": "6.10.0",
- "symfony/polyfill-intl-idn": "1.32.0",
- "symfony/polyfill-mbstring": "1.32.0",
- "symfony/yaml": "7.3.2"
+ "symfony/polyfill-intl-idn": "1.33.0",
+ "symfony/polyfill-mbstring": "1.33.0",
+ "symfony/yaml": "7.3.3"
},
"replace": {
"symfony/polyfill-php72": "*"
@@ -317,6 +313,7 @@
"ext-apcu": "Support for the Apcu cache driver",
"ext-exif": "Support for exif information from images",
"ext-fileinfo": "Improved mime type detection for files",
+ "ext-imagick": "Improved thumbnail generation",
"ext-intl": "Improved i18n number formatting",
"ext-memcached": "Support for the Memcached cache driver",
"ext-redis": "Support for the Redis cache driver",
@@ -372,7 +369,7 @@
"type": "custom"
}
],
- "time": "2025-08-19T07:39:58+00:00"
+ "time": "2025-09-16T13:06:53+00:00"
},
{
"name": "getkirby/composer-installer",
@@ -935,7 +932,7 @@
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -998,7 +995,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
},
"funding": [
{
@@ -1009,6 +1006,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1103,7 +1104,7 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -1164,7 +1165,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -1175,6 +1176,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1184,16 +1189,16 @@
},
{
"name": "symfony/yaml",
- "version": "v7.3.2",
+ "version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30"
+ "reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30",
- "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
+ "reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
"shasum": ""
},
"require": {
@@ -1236,7 +1241,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v7.3.2"
+ "source": "https://github.com/symfony/yaml/tree/v7.3.3"
},
"funding": [
{
@@ -1256,7 +1261,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:47:49+00:00"
+ "time": "2025-08-27T11:34:33+00:00"
}
],
"packages-dev": [],
diff --git a/public/kirby/cacert.pem b/public/kirby/cacert.pem
index 0dee453..f04c551 100644
--- a/public/kirby/cacert.pem
+++ b/public/kirby/cacert.pem
@@ -1,7 +1,7 @@
##
## Bundle of CA Root Certificates
##
-## Certificate data from Mozilla as of: Tue Aug 12 03:12:01 2025 GMT
+## Certificate data from Mozilla as of: Tue Sep 9 03:12:01 2025 GMT
##
## Find updated versions here: https://curl.se/docs/caextract.html
##
@@ -16,7 +16,7 @@
## Just configure this file as the SSLCACertificateFile.
##
## Conversion done with mk-ca-bundle.pl version 1.29.
-## SHA256: c185b859c19b05f104c50e1b0b2a6c775149a1d9bb731d414d73b1722892a66c
+## SHA256: 0078e6bdd280fd89e1b883174387aae84b3eae2ee263416a5f8a14ee7f179ae9
##
diff --git a/public/kirby/composer.json b/public/kirby/composer.json
index d629846..1c4dbb9 100644
--- a/public/kirby/composer.json
+++ b/public/kirby/composer.json
@@ -3,7 +3,7 @@
"description": "The Kirby core",
"license": "proprietary",
"type": "kirby-cms",
- "version": "5.0.4",
+ "version": "5.1.1",
"keywords": [
"kirby",
"cms",
@@ -38,15 +38,15 @@
"ext-openssl": "*",
"christian-riesen/base32": "1.6.0",
"claviska/simpleimage": "4.2.1",
- "composer/semver": "3.4.3",
+ "composer/semver": "3.4.4",
"filp/whoops": "2.18.4",
"getkirby/composer-installer": "^1.2.1",
"laminas/laminas-escaper": "2.17.0",
"michelf/php-smartypants": "1.8.1",
"phpmailer/phpmailer": "6.10.0",
- "symfony/polyfill-intl-idn": "1.32.0",
- "symfony/polyfill-mbstring": "1.32.0",
- "symfony/yaml": "7.3.2"
+ "symfony/polyfill-intl-idn": "1.33.0",
+ "symfony/polyfill-mbstring": "1.33.0",
+ "symfony/yaml": "7.3.3"
},
"replace": {
"symfony/polyfill-php72": "*"
@@ -56,6 +56,7 @@
"ext-apcu": "Support for the Apcu cache driver",
"ext-exif": "Support for exif information from images",
"ext-fileinfo": "Improved mime type detection for files",
+ "ext-imagick": "Improved thumbnail generation",
"ext-intl": "Improved i18n number formatting",
"ext-memcached": "Support for the Memcached cache driver",
"ext-redis": "Support for the Redis cache driver",
diff --git a/public/kirby/composer.lock b/public/kirby/composer.lock
index a9a7b86..29f7601 100644
--- a/public/kirby/composer.lock
+++ b/public/kirby/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "dcb9ca6ed7a038f2028aba54fada5f7b",
+ "content-hash": "8d08002d38005e6ec1ff6a97deac20e2",
"packages": [
{
"name": "christian-riesen/base32",
@@ -120,16 +120,16 @@
},
{
"name": "composer/semver",
- "version": "3.4.3",
+ "version": "3.4.4",
"source": {
"type": "git",
"url": "https://github.com/composer/semver.git",
- "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12"
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
- "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12",
+ "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
"shasum": ""
},
"require": {
@@ -181,7 +181,7 @@
"support": {
"irc": "ircs://irc.libera.chat:6697/composer",
"issues": "https://github.com/composer/semver/issues",
- "source": "https://github.com/composer/semver/tree/3.4.3"
+ "source": "https://github.com/composer/semver/tree/3.4.4"
},
"funding": [
{
@@ -191,13 +191,9 @@
{
"url": "https://github.com/composer",
"type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
}
],
- "time": "2024-09-19T14:15:21+00:00"
+ "time": "2025-08-20T19:15:30+00:00"
},
{
"name": "filp/whoops",
@@ -693,7 +689,7 @@
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
@@ -752,7 +748,7 @@
"portable"
],
"support": {
- "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0"
},
"funding": [
{
@@ -763,6 +759,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -772,7 +772,7 @@
},
{
"name": "symfony/polyfill-intl-idn",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
@@ -835,7 +835,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0"
},
"funding": [
{
@@ -846,6 +846,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -855,7 +859,7 @@
},
{
"name": "symfony/polyfill-intl-normalizer",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
@@ -916,7 +920,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0"
},
"funding": [
{
@@ -927,6 +931,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -936,7 +944,7 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
+ "version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
@@ -997,7 +1005,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0"
},
"funding": [
{
@@ -1008,6 +1016,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -1017,16 +1029,16 @@
},
{
"name": "symfony/yaml",
- "version": "v7.3.2",
+ "version": "v7.3.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30"
+ "reference": "d4f4a66866fe2451f61296924767280ab5732d9d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/b8d7d868da9eb0919e99c8830431ea087d6aae30",
- "reference": "b8d7d868da9eb0919e99c8830431ea087d6aae30",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d",
+ "reference": "d4f4a66866fe2451f61296924767280ab5732d9d",
"shasum": ""
},
"require": {
@@ -1069,7 +1081,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v7.3.2"
+ "source": "https://github.com/symfony/yaml/tree/v7.3.3"
},
"funding": [
{
@@ -1089,7 +1101,7 @@
"type": "tidelift"
}
],
- "time": "2025-07-10T08:47:49+00:00"
+ "time": "2025-08-27T11:34:33+00:00"
}
],
"packages-dev": [],
diff --git a/public/kirby/config/areas/account/dialogs.php b/public/kirby/config/areas/account/dialogs.php
index eef1440..fbdf0e8 100644
--- a/public/kirby/config/areas/account/dialogs.php
+++ b/public/kirby/config/areas/account/dialogs.php
@@ -5,101 +5,61 @@ use Kirby\Panel\UserTotpEnableDialog;
$dialogs = require __DIR__ . '/../users/dialogs.php';
return [
- // change email
'account.changeEmail' => [
+ ...$dialogs['user.changeEmail'],
'pattern' => '(account)/changeEmail',
- 'load' => $dialogs['user.changeEmail']['load'],
- 'submit' => $dialogs['user.changeEmail']['submit'],
],
-
- // change language
'account.changeLanguage' => [
+ ...$dialogs['user.changeLanguage'],
'pattern' => '(account)/changeLanguage',
- 'load' => $dialogs['user.changeLanguage']['load'],
- 'submit' => $dialogs['user.changeLanguage']['submit'],
],
-
- // change name
'account.changeName' => [
+ ...$dialogs['user.changeName'],
'pattern' => '(account)/changeName',
- 'load' => $dialogs['user.changeName']['load'],
- 'submit' => $dialogs['user.changeName']['submit'],
],
-
- // change password
'account.changePassword' => [
+ ...$dialogs['user.changePassword'],
'pattern' => '(account)/changePassword',
- 'load' => $dialogs['user.changePassword']['load'],
- 'submit' => $dialogs['user.changePassword']['submit'],
],
-
- // change role
'account.changeRole' => [
+ ...$dialogs['user.changeRole'],
'pattern' => '(account)/changeRole',
- 'load' => $dialogs['user.changeRole']['load'],
- 'submit' => $dialogs['user.changeRole']['submit'],
],
-
- // delete
'account.delete' => [
+ ...$dialogs['user.delete'],
'pattern' => '(account)/delete',
- 'load' => $dialogs['user.delete']['load'],
- 'submit' => $dialogs['user.delete']['submit'],
],
-
- // account fields dialogs
'account.fields' => [
- 'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $dialogs['user.fields']['load'],
- 'submit' => $dialogs['user.fields']['submit']
+ ...$dialogs['user.fields'],
+ 'pattern' => '(account)/fields/(:any)/(:all?)',
],
-
- // change file name
'account.file.changeName' => [
+ ...$dialogs['user.file.changeName'],
'pattern' => '(account)/files/(:any)/changeName',
- 'load' => $dialogs['user.file.changeName']['load'],
- 'submit' => $dialogs['user.file.changeName']['submit'],
],
-
- // change file sort
'account.file.changeSort' => [
+ ...$dialogs['user.file.changeSort'],
'pattern' => '(account)/files/(:any)/changeSort',
- 'load' => $dialogs['user.file.changeSort']['load'],
- 'submit' => $dialogs['user.file.changeSort']['submit'],
],
-
- // change file template
'account.file.changeTemplate' => [
+ ...$dialogs['user.file.changeTemplate'],
'pattern' => '(account)/files/(:any)/changeTemplate',
- 'load' => $dialogs['user.file.changeTemplate']['load'],
- 'submit' => $dialogs['user.file.changeTemplate']['submit'],
],
-
- // delete
'account.file.delete' => [
+ ...$dialogs['user.file.delete'],
'pattern' => '(account)/files/(:any)/delete',
- 'load' => $dialogs['user.file.delete']['load'],
- 'submit' => $dialogs['user.file.delete']['submit'],
],
-
- // account file fields dialogs
'account.file.fields' => [
+ ...$dialogs['user.file.fields'],
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $dialogs['user.file.fields']['load'],
- 'submit' => $dialogs['user.file.fields']['submit']
],
-
- // account enable TOTP
'account.totp.enable' => [
'pattern' => '(account)/totp/enable',
'load' => fn () => (new UserTotpEnableDialog())->load(),
'submit' => fn () => (new UserTotpEnableDialog())->submit()
],
-
- // account disable TOTP
'account.totp.disable' => [
'pattern' => '(account)/totp/disable',
- 'load' => $dialogs['user.totp.disable']['load'],
- 'submit' => $dialogs['user.totp.disable']['submit']
+ ...$dialogs['user.totp.disable'],
],
];
diff --git a/public/kirby/config/areas/account/drawers.php b/public/kirby/config/areas/account/drawers.php
index 01bb0b6..714d6c5 100644
--- a/public/kirby/config/areas/account/drawers.php
+++ b/public/kirby/config/areas/account/drawers.php
@@ -3,17 +3,12 @@
$drawers = require __DIR__ . '/../users/drawers.php';
return [
- // account fields drawers
'account.fields' => [
- 'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $drawers['user.fields']['load'],
- 'submit' => $drawers['user.fields']['submit']
+ ...$drawers['user.fields'],
+ 'pattern' => '(account)/fields/(:any)/(:all?)',
],
-
- // account file fields drawers
'account.file.fields' => [
+ ...$drawers['user.file.fields'],
'pattern' => '(account)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $drawers['user.file.fields']['load'],
- 'submit' => $drawers['user.file.fields']['submit']
],
];
diff --git a/public/kirby/config/areas/account/dropdowns.php b/public/kirby/config/areas/account/dropdowns.php
index 6d115d5..6011c30 100644
--- a/public/kirby/config/areas/account/dropdowns.php
+++ b/public/kirby/config/areas/account/dropdowns.php
@@ -4,19 +4,19 @@ $dropdowns = require __DIR__ . '/../users/dropdowns.php';
return [
'account' => [
+ ...$dropdowns['user'],
'pattern' => '(account)',
- 'options' => $dropdowns['user']['options']
],
'account.languages' => [
+ ...$dropdowns['user.languages'],
'pattern' => '(account)/languages',
- 'options' => $dropdowns['user.languages']['options']
],
'account.file' => [
+ ...$dropdowns['user.file'],
'pattern' => '(account)/files/(:any)',
- 'options' => $dropdowns['user.file']['options']
],
'account.file.languages' => [
+ ...$dropdowns['user.file.languages'],
'pattern' => '(account)/files/(:any)/languages',
- 'options' => $files['language']
]
];
diff --git a/public/kirby/config/areas/lab/drawers.php b/public/kirby/config/areas/lab/drawers.php
index d1e515b..eea790d 100644
--- a/public/kirby/config/areas/lab/drawers.php
+++ b/public/kirby/config/areas/lab/drawers.php
@@ -16,12 +16,14 @@ return [
];
}
+ $doc = Doc::factory($component);
+
return [
'component' => 'k-lab-docs-drawer',
'props' => [
'icon' => 'book',
'title' => $component,
- 'docs' => Doc::factory($component)->toArray()
+ 'docs' => $doc->toArray()
]
];
},
diff --git a/public/kirby/config/areas/site/dialogs.php b/public/kirby/config/areas/site/dialogs.php
index 6b2a881..c9033c9 100644
--- a/public/kirby/config/areas/site/dialogs.php
+++ b/public/kirby/config/areas/site/dialogs.php
@@ -20,8 +20,6 @@ $fields = require __DIR__ . '/../fields/dialogs.php';
$files = require __DIR__ . '/../files/dialogs.php';
return [
-
- // change page position
'page.changeSort' => [
'pattern' => 'pages/(:any)/changeSort',
'load' => function (string $id) {
@@ -61,7 +59,6 @@ return [
}
],
- // change page status
'page.changeStatus' => [
'pattern' => 'pages/(:any)/changeStatus',
'load' => function (string $id) {
@@ -140,7 +137,6 @@ return [
}
],
- // change template
'page.changeTemplate' => [
'pattern' => 'pages/(:any)/changeTemplate',
'load' => function (string $id) {
@@ -187,7 +183,6 @@ return [
}
],
- // change title
'page.changeTitle' => [
'pattern' => 'pages/(:any)/changeTitle',
'load' => function (string $id) {
@@ -282,7 +277,6 @@ return [
}
],
- // create a new page
'page.create' => [
'pattern' => 'pages/create',
'load' => function () {
@@ -293,6 +287,7 @@ return [
slug: $request->get('slug'),
template: $request->get('template'),
title: $request->get('title'),
+ uuid: $request->get('uuid'),
viewId: $request->get('view'),
);
@@ -306,6 +301,7 @@ return [
slug: $request->get('slug'),
template: $request->get('template'),
title: $request->get('title'),
+ uuid: $request->get('uuid'),
viewId: $request->get('view'),
);
@@ -313,7 +309,6 @@ return [
}
],
- // delete page
'page.delete' => [
'pattern' => 'pages/(:any)/delete',
'load' => function (string $id) {
@@ -385,7 +380,6 @@ return [
}
],
- // duplicate page
'page.duplicate' => [
'pattern' => 'pages/(:any)/duplicate',
'load' => function (string $id) {
@@ -474,49 +468,31 @@ return [
}
],
- // page field dialogs
'page.fields' => [
- 'pattern' => '(pages/.*?)/fields/(:any)/(:all?)',
- 'load' => $fields['model']['load'],
- 'submit' => $fields['model']['submit']
+ ...$fields['model'],
+ 'pattern' => '(pages/[^/]+)/fields/(:any)/(:all?)',
],
-
- // change filename
'page.file.changeName' => [
- 'pattern' => '(pages/.*?)/files/(:any)/changeName',
- 'load' => $files['changeName']['load'],
- 'submit' => $files['changeName']['submit'],
+ ...$files['changeName'],
+ 'pattern' => '(pages/[^/]+)/files/(:any)/changeName',
],
-
- // change sort
'page.file.changeSort' => [
- 'pattern' => '(pages/.*?)/files/(:any)/changeSort',
- 'load' => $files['changeSort']['load'],
- 'submit' => $files['changeSort']['submit'],
+ ...$files['changeSort'],
+ 'pattern' => '(pages/[^/]+)/files/(:any)/changeSort',
],
-
- // change template
'page.file.changeTemplate' => [
- 'pattern' => '(pages/.*?)/files/(:any)/changeTemplate',
- 'load' => $files['changeTemplate']['load'],
- 'submit' => $files['changeTemplate']['submit'],
+ ...$files['changeTemplate'],
+ 'pattern' => '(pages/[^/]+)/files/(:any)/changeTemplate',
],
-
- // delete
'page.file.delete' => [
- 'pattern' => '(pages/.*?)/files/(:any)/delete',
- 'load' => $files['delete']['load'],
- 'submit' => $files['delete']['submit'],
+ ...$files['delete'],
+ 'pattern' => '(pages/[^/]+)/files/(:any)/delete',
],
-
- // page file field dialogs
'page.file.fields' => [
- 'pattern' => '(pages/.*?)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $fields['file']['load'],
- 'submit' => $fields['file']['submit'],
+ ...$fields['file'],
+ 'pattern' => '(pages/[^/]+)/files/(:any)/fields/(:any)/(:all?)',
],
- // move page
'page.move' => [
'pattern' => 'pages/(:any)/move',
'load' => function (string $id) {
@@ -553,7 +529,6 @@ return [
}
],
- // change site title
'site.changeTitle' => [
'pattern' => 'site/changeTitle',
'load' => function () {
@@ -583,49 +558,31 @@ return [
}
],
- // site field dialogs
'site.fields' => [
+ ...$fields['model'],
'pattern' => '(site)/fields/(:any)/(:all?)',
- 'load' => $fields['model']['load'],
- 'submit' => $fields['model']['submit'],
],
-
- // change filename
'site.file.changeName' => [
+ ...$files['changeName'],
'pattern' => '(site)/files/(:any)/changeName',
- 'load' => $files['changeName']['load'],
- 'submit' => $files['changeName']['submit'],
],
-
- // change sort
'site.file.changeSort' => [
+ ...$files['changeSort'],
'pattern' => '(site)/files/(:any)/changeSort',
- 'load' => $files['changeSort']['load'],
- 'submit' => $files['changeSort']['submit'],
],
-
- // change template
'site.file.changeTemplate' => [
+ ...$files['changeTemplate'],
'pattern' => '(site)/files/(:any)/changeTemplate',
- 'load' => $files['changeTemplate']['load'],
- 'submit' => $files['changeTemplate']['submit'],
],
-
- // delete
'site.file.delete' => [
+ ...$files['delete'],
'pattern' => '(site)/files/(:any)/delete',
- 'load' => $files['delete']['load'],
- 'submit' => $files['delete']['submit'],
],
-
- // site file field dialogs
'site.file.fields' => [
+ ...$fields['file'],
'pattern' => '(site)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $fields['file']['load'],
- 'submit' => $fields['file']['submit'],
],
- // content changes
'changes' => [
'pattern' => 'changes',
'load' => function () {
diff --git a/public/kirby/config/areas/site/drawers.php b/public/kirby/config/areas/site/drawers.php
index 7bdf4da..86d2162 100644
--- a/public/kirby/config/areas/site/drawers.php
+++ b/public/kirby/config/areas/site/drawers.php
@@ -3,31 +3,20 @@
$fields = require __DIR__ . '/../fields/drawers.php';
return [
- // page field drawers
'page.fields' => [
- 'pattern' => '(pages/.*?)/fields/(:any)/(:all?)',
- 'load' => $fields['model']['load'],
- 'submit' => $fields['model']['submit']
+ ...$fields['model'],
+ 'pattern' => '(pages/[^/]+)/fields/(:any)/(:all?)',
],
-
- // page file field drawers
'page.file.fields' => [
- 'pattern' => '(pages/.*?)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $fields['file']['load'],
- 'submit' => $fields['file']['submit'],
+ ...$fields['file'],
+ 'pattern' => '(pages/[^/]+)/files/(:any)/fields/(:any)/(:all?)',
],
-
- // site field drawers
'site.fields' => [
+ ...$fields['model'],
'pattern' => '(site)/fields/(:any)/(:all?)',
- 'load' => $fields['model']['load'],
- 'submit' => $fields['model']['submit'],
],
-
- // site file field drawers
'site.file.fields' => [
+ ...$fields['file'],
'pattern' => '(site)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $fields['file']['load'],
- 'submit' => $fields['file']['submit'],
],
];
diff --git a/public/kirby/config/areas/site/dropdowns.php b/public/kirby/config/areas/site/dropdowns.php
index 207e028..363b4b0 100644
--- a/public/kirby/config/areas/site/dropdowns.php
+++ b/public/kirby/config/areas/site/dropdowns.php
@@ -21,11 +21,11 @@ return [
}
],
'page.file' => [
- 'pattern' => '(pages/.*?)/files/(:any)',
+ 'pattern' => '(pages/[^/]+)/files/(:any)',
'options' => $files['file']
],
'page.file.languages' => [
- 'pattern' => '(pages/.*?)/files/(:any)/languages',
+ 'pattern' => '(pages/[^/]+)/files/(:any)/languages',
'options' => $files['language']
],
'site.languages' => [
diff --git a/public/kirby/config/areas/system/dialogs.php b/public/kirby/config/areas/system/dialogs.php
index db8b7a5..8421a5f 100644
--- a/public/kirby/config/areas/system/dialogs.php
+++ b/public/kirby/config/areas/system/dialogs.php
@@ -57,6 +57,28 @@ return [
// @codeCoverageIgnoreEnd
}
],
+ 'license/remove' => [
+ 'load' => function () {
+ return [
+ 'component' => 'k-remove-dialog',
+ 'props' => [
+ 'text' => I18n::translate('license.remove.text'),
+ 'size' => 'medium',
+ 'submitButton' => [
+ 'icon' => 'trash',
+ 'text' => I18n::translate('remove'),
+ 'theme' => 'negative',
+ ],
+ ]
+ ];
+ },
+ 'submit' => function () {
+ // @codeCoverageIgnoreStart
+ App::instance()->system()->license()->delete();
+ return true;
+ // @codeCoverageIgnoreEnd
+ }
+ ],
// license registration
'registration' => [
'load' => function () {
diff --git a/public/kirby/config/areas/users/dialogs.php b/public/kirby/config/areas/users/dialogs.php
index 555d38d..afadefc 100644
--- a/public/kirby/config/areas/users/dialogs.php
+++ b/public/kirby/config/areas/users/dialogs.php
@@ -15,8 +15,6 @@ $fields = require __DIR__ . '/../fields/dialogs.php';
$files = require __DIR__ . '/../files/dialogs.php';
return [
-
- // create
'user.create' => [
'pattern' => 'users/create',
'load' => function () {
@@ -79,7 +77,6 @@ return [
}
],
- // change email
'user.changeEmail' => [
'pattern' => 'users/(:any)/changeEmail',
'load' => function (string $id) {
@@ -114,7 +111,6 @@ return [
}
],
- // change language
'user.changeLanguage' => [
'pattern' => 'users/(:any)/changeLanguage',
'load' => function (string $id) {
@@ -147,7 +143,6 @@ return [
}
],
- // change name
'user.changeName' => [
'pattern' => 'users/(:any)/changeName',
'load' => function (string $id) {
@@ -179,7 +174,6 @@ return [
}
],
- // change password
'user.changePassword' => [
'pattern' => 'users/(:any)/changePassword',
'load' => function (string $id) {
@@ -245,7 +239,6 @@ return [
}
],
- // change role
'user.changeRole' => [
'pattern' => 'users/(:any)/changeRole',
'load' => function (string $id) {
@@ -282,7 +275,6 @@ return [
}
],
- // delete
'user.delete' => [
'pattern' => 'users/(:any)/delete',
'load' => function (string $id) {
@@ -324,49 +316,36 @@ return [
}
],
- // user field dialogs
'user.fields' => [
- 'pattern' => '(users/.*?)/fields/(:any)/(:all?)',
- 'load' => $fields['model']['load'],
- 'submit' => $fields['model']['submit']
+ ...$fields['model'],
+ 'pattern' => '(users/[^/]+)/fields/(:any)/(:all?)',
],
- // change file name
'user.file.changeName' => [
- 'pattern' => '(users/.*?)/files/(:any)/changeName',
- 'load' => $files['changeName']['load'],
- 'submit' => $files['changeName']['submit'],
+ ...$files['changeName'],
+ 'pattern' => '(users/[^/]+)/files/(:any)/changeName',
],
- // change file sort
'user.file.changeSort' => [
- 'pattern' => '(users/.*?)/files/(:any)/changeSort',
- 'load' => $files['changeSort']['load'],
- 'submit' => $files['changeSort']['submit'],
+ ...$files['changeSort'],
+ 'pattern' => '(users/[^/]+)/files/(:any)/changeSort',
],
- // change file template
'user.file.changeTemplate' => [
- 'pattern' => '(users/.*?)/files/(:any)/changeTemplate',
- 'load' => $files['changeTemplate']['load'],
- 'submit' => $files['changeTemplate']['submit'],
+ ...$files['changeTemplate'],
+ 'pattern' => '(users/[^/]+)/files/(:any)/changeTemplate',
],
- // delete file
'user.file.delete' => [
- 'pattern' => '(users/.*?)/files/(:any)/delete',
- 'load' => $files['delete']['load'],
- 'submit' => $files['delete']['submit'],
+ ...$files['delete'],
+ 'pattern' => '(users/[^/]+)/files/(:any)/delete',
],
- // user file fields dialogs
'user.file.fields' => [
- 'pattern' => '(users/.*?)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $fields['file']['load'],
- 'submit' => $fields['file']['submit']
+ ...$fields['file'],
+ 'pattern' => '(users/[^/]+)/files/(:any)/fields/(:any)/(:all?)',
],
- // user disable TOTP
'user.totp.disable' => [
'pattern' => 'users/(:any)/totp/disable',
'load' => fn (string $id) => (new UserTotpDisableDialog($id))->load(),
diff --git a/public/kirby/config/areas/users/drawers.php b/public/kirby/config/areas/users/drawers.php
index 10d6bd1..de1f9e8 100644
--- a/public/kirby/config/areas/users/drawers.php
+++ b/public/kirby/config/areas/users/drawers.php
@@ -3,16 +3,12 @@
$fields = require __DIR__ . '/../fields/drawers.php';
return [
- // user field drawers
'user.fields' => [
- 'pattern' => '(users/.*?)/fields/(:any)/(:all?)',
- 'load' => $fields['model']['load'],
- 'submit' => $fields['model']['submit']
+ ...$fields['model'],
+ 'pattern' => '(users/[^/]+)/fields/(:any)/(:all?)',
],
- // user file fields drawers
'user.file.fields' => [
- 'pattern' => '(users/.*?)/files/(:any)/fields/(:any)/(:all?)',
- 'load' => $fields['file']['load'],
- 'submit' => $fields['file']['submit']
+ ...$fields['file'],
+ 'pattern' => '(users/[^/]+)/files/(:any)/fields/(:any)/(:all?)',
],
];
diff --git a/public/kirby/config/areas/users/dropdowns.php b/public/kirby/config/areas/users/dropdowns.php
index d3d2569..6ab5556 100644
--- a/public/kirby/config/areas/users/dropdowns.php
+++ b/public/kirby/config/areas/users/dropdowns.php
@@ -19,11 +19,11 @@ return [
}
],
'user.file' => [
- 'pattern' => '(users/.*?)/files/(:any)',
+ 'pattern' => '(users/[^/]+)/files/(:any)',
'options' => $files['file']
],
'user.file.languages' => [
- 'pattern' => '(users/.*?)/files/(:any)/languages',
+ 'pattern' => '(users/[^/]+)/files/(:any)/languages',
'options' => $files['language']
]
];
diff --git a/public/kirby/config/areas/users/views.php b/public/kirby/config/areas/users/views.php
index cf4cb7e..1f99eda 100644
--- a/public/kirby/config/areas/users/views.php
+++ b/public/kirby/config/areas/users/views.php
@@ -2,8 +2,9 @@
use Kirby\Cms\App;
use Kirby\Cms\Find;
+use Kirby\Panel\Collector\UsersCollector;
use Kirby\Panel\Ui\Buttons\ViewButtons;
-use Kirby\Toolkit\Escape;
+use Kirby\Panel\Ui\Item\UserItem;
return [
'users' => [
@@ -31,29 +32,17 @@ return [
},
'roles' => array_values($roles),
'users' => function () use ($kirby, $role) {
- $users = $kirby->users();
+ $collector = new UsersCollector(
+ limit: 20,
+ page: $kirby->request()->get('page', 1),
+ role: $role,
+ sortBy: 'username asc',
+ );
- if (empty($role) === false) {
- $users = $users->role($role);
- }
-
- // sort users alphabetically
- $users = $users->sortBy('username', 'asc');
-
- // paginate
- $users = $users->paginate([
- 'limit' => 20,
- 'page' => $kirby->request()->get('page')
- ]);
+ $users = $collector->models(paginated: true);
return [
- 'data' => $users->values(fn ($user) => [
- 'id' => $user->id(),
- 'image' => $user->panel()->image(),
- 'info' => Escape::html($user->role()->title()),
- 'link' => $user->panel()->url(true),
- 'text' => Escape::html($user->username())
- ]),
+ 'data' => $users->values(fn ($user) => (new UserItem(user: $user))->props()),
'pagination' => $users->pagination()->toArray()
];
},
diff --git a/public/kirby/config/components.php b/public/kirby/config/components.php
index f95ecfa..012e084 100644
--- a/public/kirby/config/components.php
+++ b/public/kirby/config/components.php
@@ -421,10 +421,7 @@ return [
// support UUIDs
if (
$path !== null &&
- (
- Uuid::is($path, 'page') === true ||
- Uuid::is($path, 'file') === true
- )
+ Uuid::is($path, ['page', 'file']) === true
) {
$model = Uuid::for($path)->model();
diff --git a/public/kirby/config/fields/object.php b/public/kirby/config/fields/object.php
index d795d28..f10516f 100644
--- a/public/kirby/config/fields/object.php
+++ b/public/kirby/config/fields/object.php
@@ -50,7 +50,7 @@ return [
return [];
}
- return $this->form()->fields()->toArray();
+ return $this->form()->fields()->toProps();
},
'value' => function () {
$data = Data::decode($this->value, 'yaml');
diff --git a/public/kirby/config/fields/structure.php b/public/kirby/config/fields/structure.php
index 3fb05d2..1b6ede0 100644
--- a/public/kirby/config/fields/structure.php
+++ b/public/kirby/config/fields/structure.php
@@ -19,6 +19,13 @@ return [
'icon' => null,
'placeholder' => null,
+ /**
+ * Whether to enable batch editing
+ */
+ 'batch' => function (bool $batch = false) {
+ return $batch;
+ },
+
/**
* Optional columns definition to only show selected fields in the structure table.
*/
@@ -105,7 +112,7 @@ return [
return [];
}
- return $this->form()->fields()->toArray();
+ return $this->form()->fields()->toProps();
},
'columns' => function () {
$columns = [];
diff --git a/public/kirby/config/fields/users.php b/public/kirby/config/fields/users.php
index f30f6ab..4533bba 100644
--- a/public/kirby/config/fields/users.php
+++ b/public/kirby/config/fields/users.php
@@ -63,12 +63,12 @@ return [
$users = [];
$kirby = App::instance();
- foreach (Data::decode($value, 'yaml') as $email) {
- if (is_array($email) === true) {
- $email = $email['email'] ?? null;
+ foreach (Data::decode($value, 'yaml') as $id) {
+ if (is_array($id) === true) {
+ $id = $id['uuid'] ?? $id['id'] ?? $id['email'] ?? null;
}
- if ($email !== null && ($user = $kirby->user($email))) {
+ if ($id !== null && ($user = $kirby->user($id))) {
$users[] = $this->userResponse($user);
}
}
diff --git a/public/kirby/config/sections/files.php b/public/kirby/config/sections/files.php
index 254e684..b362849 100644
--- a/public/kirby/config/sections/files.php
+++ b/public/kirby/config/sections/files.php
@@ -1,7 +1,9 @@
function () {
return $this->parentModel();
},
+ 'collector' => function () {
+ return $this->collector ??= new FilesCollector(
+ flip: $this->flip(),
+ limit: $this->limit(),
+ page: $this->page() ?? 1,
+ parent: $this->parent(),
+ query: $this->query(),
+ search: $this->searchterm(),
+ sortBy: $this->sortBy(),
+ template: $this->template(),
+ );
+ },
'models' => function () {
- if ($this->query !== null) {
- $files = $this->parent->query($this->query, Files::class) ?? new Files([]);
- } else {
- $files = $this->parent->files();
- }
-
- // filter files by template
- $files = $files->template($this->template);
-
- // filter out all protected and hidden files
- $files = $files->filter('isListable', true);
-
- // search
- if ($this->search === true && empty($this->searchterm()) === false) {
- $files = $files->search($this->searchterm());
-
- // disable flip and sortBy while searching
- // to show most relevant results
- $this->flip = false;
- $this->sortBy = null;
- }
-
- // sort
- if ($this->sortBy) {
- $files = $files->sort(...$files::sortArgs($this->sortBy));
- } else {
- $files = $files->sorted();
- }
-
- // flip
- if ($this->flip === true) {
- $files = $files->flip();
- }
-
- return $files;
+ return $this->collector()->models();
},
'modelsPaginated' => function () {
- // apply the default pagination
- return $this->models()->paginate([
- 'page' => $this->page,
- 'limit' => $this->limit,
- 'method' => 'none' // the page is manually provided
- ]);
+ return $this->collector()->models(paginated: true);
},
'files' => function () {
return $this->models;
},
'data' => function () {
- $data = [];
+ $data = [];
+ $dragTextIsAbsolute = $this->model->is($this->parent) === false;
foreach ($this->modelsPaginated() as $file) {
- $panel = $file->panel();
- $permissions = $file->permissions();
-
- $item = [
- 'dragText' => $panel->dragText(
- // the drag text needs to be absolute
- // when the files come from a different parent model
- absolute: $this->model->is($this->parent) === false
- ),
- 'extension' => $file->extension(),
- 'filename' => $file->filename(),
- 'id' => $file->id(),
- 'image' => $panel->image(
- $this->image,
- $this->layout === 'table' ? 'list' : $this->layout
- ),
- 'info' => $file->toSafeString($this->info ?? false),
- 'link' => $panel->url(true),
- 'mime' => $file->mime(),
- 'parent' => $file->parent()->panel()->path(),
- 'permissions' => [
- 'delete' => $permissions->can('delete'),
- 'sort' => $permissions->can('sort'),
- ],
- 'template' => $file->template(),
- 'text' => $file->toSafeString($this->text),
- 'url' => $file->url(),
- ];
+ $item = (new FileItem(
+ file: $file,
+ dragTextIsAbsolute: $dragTextIsAbsolute,
+ image: $this->image,
+ layout: $this->layout,
+ info: $this->info,
+ text: $this->text,
+ ))->props();
if ($this->layout === 'table') {
$item = $this->columnsValues($item, $file);
@@ -185,26 +141,16 @@ return [
return false;
}
- // count all uploaded files
- $max = $this->max ? $this->max - $this->total : null;
- $multiple = !$max || $max > 1;
- $template = $this->template === 'default' ? null : $this->template;
+ $settings = new Upload(
+ api: $this->parent->apiUrl(true) . '/files',
+ accept: $this->accept,
+ max: $this->max ? $this->max - $this->total : null,
+ preview: $this->image,
+ sort: $this->sortable === true ? $this->total + 1 : null,
+ template: $this->template,
+ );
- return [
- 'accept' => $this->accept,
- 'multiple' => $multiple,
- 'max' => $max,
- 'api' => $this->parent->apiUrl(true) . '/files',
- 'preview' => $this->image,
- 'attributes' => [
- // TODO: an edge issue that needs to be solved:
- // if multiple users load the same section
- // at the same time and upload a file,
- // uploaded files have the same sort number
- 'sort' => $this->sortable === true ? $this->total + 1 : null,
- 'template' => $template
- ]
- ];
+ return $settings->props();
}
],
// @codeCoverageIgnoreStart
diff --git a/public/kirby/config/sections/mixins/search.php b/public/kirby/config/sections/mixins/search.php
index 0791152..05fb50f 100644
--- a/public/kirby/config/sections/mixins/search.php
+++ b/public/kirby/config/sections/mixins/search.php
@@ -13,7 +13,11 @@ return [
],
'methods' => [
'searchterm' => function (): string|null {
- return App::instance()->request()->get('searchterm');
+ if ($this->search() === true) {
+ return App::instance()->request()->get('searchterm') ?? null;
+ }
+
+ return null;
}
]
];
diff --git a/public/kirby/config/sections/pages.php b/public/kirby/config/sections/pages.php
index fe3d47c..cfa39aa 100644
--- a/public/kirby/config/sections/pages.php
+++ b/public/kirby/config/sections/pages.php
@@ -5,6 +5,8 @@ use Kirby\Cms\Page;
use Kirby\Cms\Pages;
use Kirby\Cms\Site;
use Kirby\Exception\InvalidArgumentException;
+use Kirby\Panel\Collector\PagesCollector;
+use Kirby\Panel\Ui\Item\PageItem;
use Kirby\Toolkit\A;
use Kirby\Toolkit\I18n;
@@ -85,83 +87,28 @@ return [
return $parent;
},
+ 'collector' => function () {
+ return $this->collector ??= new PagesCollector(
+ limit: $this->limit(),
+ page: $this->page() ?? 1,
+ parent: $this->parent(),
+ query: $this->query(),
+ status: $this->status(),
+ templates: $this->templates(),
+ templatesIgnore: $this->templatesIgnore(),
+ search: $this->searchterm(),
+ sortBy: $this->sortBy(),
+ flip: $this->flip()
+ );
+ },
'models' => function () {
- if ($this->query !== null) {
- $pages = $this->parent->query($this->query, Pages::class) ?? new Pages([]);
- } else {
- $pages = match ($this->status) {
- 'draft' => $this->parent->drafts(),
- 'listed' => $this->parent->children()->listed(),
- 'published' => $this->parent->children(),
- 'unlisted' => $this->parent->children()->unlisted(),
- default => $this->parent->childrenAndDrafts()
- };
- }
-
- // filters pages that are protected and not in the templates list
- // internal `filter()` method used instead of foreach loop that previously included `unset()`
- // because `unset()` is updating the original data, `filter()` is just filtering
- // also it has been tested that there is no performance difference
- // even in 0.1 seconds on 100k virtual pages
- $pages = $pages->filter(function ($page) {
- // remove all protected and hidden pages
- if ($page->isListable() === false) {
- return false;
- }
-
- $intendedTemplate = $page->intendedTemplate()->name();
-
- // filter by all set templates
- if (
- $this->templates &&
- in_array($intendedTemplate, $this->templates, true) === false
- ) {
- return false;
- }
-
- // exclude by all ignored templates
- if (
- $this->templatesIgnore &&
- in_array($intendedTemplate, $this->templatesIgnore, true) === true
- ) {
- return false;
- }
-
- return true;
- });
-
- // search
- if ($this->search === true && empty($this->searchterm()) === false) {
- $pages = $pages->search($this->searchterm());
-
- // disable flip and sortBy while searching
- // to show most relevant results
- $this->flip = false;
- $this->sortBy = null;
- }
-
- // sort
- if ($this->sortBy) {
- $pages = $pages->sort(...$pages::sortArgs($this->sortBy));
- }
-
- // flip
- if ($this->flip === true) {
- $pages = $pages->flip();
- }
-
- return $pages;
+ return $this->collector()->models();
},
'modelsPaginated' => function () {
- // pagination
- return $this->models()->paginate([
- 'page' => $this->page,
- 'limit' => $this->limit,
- 'method' => 'none' // the page is manually provided
- ]);
+ return $this->collector()->models(paginated: true);
},
'pages' => function () {
- return $this->models;
+ return $this->models();
},
'total' => function () {
return $this->models()->count();
@@ -170,30 +117,13 @@ return [
$data = [];
foreach ($this->modelsPaginated() as $page) {
- $panel = $page->panel();
- $permissions = $page->permissions();
-
- $item = [
- 'dragText' => $panel->dragText(),
- 'id' => $page->id(),
- 'image' => $panel->image(
- $this->image,
- $this->layout === 'table' ? 'list' : $this->layout
- ),
- 'info' => $page->toSafeString($this->info ?? false),
- 'link' => $panel->url(true),
- 'parent' => $page->parentId(),
- 'permissions' => [
- 'delete' => $permissions->can('delete'),
- 'changeSlug' => $permissions->can('changeSlug'),
- 'changeStatus' => $permissions->can('changeStatus'),
- 'changeTitle' => $permissions->can('changeTitle'),
- 'sort' => $permissions->can('sort'),
- ],
- 'status' => $page->status(),
- 'template' => $page->intendedTemplate()->name(),
- 'text' => $page->toSafeString($this->text),
- ];
+ $item = (new PageItem(
+ page: $page,
+ image: $this->image,
+ layout: $this->layout,
+ info: $this->info,
+ text: $this->text,
+ ))->props();
if ($this->layout === 'table') {
$item = $this->columnsValues($item, $page);
diff --git a/public/kirby/config/sections/stats.php b/public/kirby/config/sections/stats.php
index 86efe96..f5e06fd 100644
--- a/public/kirby/config/sections/stats.php
+++ b/public/kirby/config/sections/stats.php
@@ -1,6 +1,6 @@
[
@@ -10,20 +10,8 @@ return [
/**
* Array or query string for reports. Each report needs a `label` and `value` and can have additional `info`, `link`, `icon` and `theme` settings.
*/
- 'reports' => function ($reports = null) {
- if ($reports === null) {
- return [];
- }
-
- if (is_string($reports) === true) {
- $reports = $this->model()->query($reports);
- }
-
- if (is_array($reports) === false) {
- return [];
- }
-
- return $reports;
+ 'reports' => function (array|string|null $reports = null) {
+ return $reports ?? [];
},
/**
* The size of the report cards. Available sizes: `tiny`, `small`, `medium`, `large`
@@ -33,36 +21,18 @@ return [
}
],
'computed' => [
- 'reports' => function () {
- $reports = [];
- $model = $this->model();
- $toString = fn ($value) => $value === null ? null : $model->toString($value);
-
- foreach ($this->reports as $report) {
- if (is_string($report) === true) {
- $report = $model->query($report);
- }
-
- if (is_array($report) === false) {
- continue;
- }
-
- $info = $report['info'] ?? null;
- $label = $report['label'] ?? null;
- $link = $report['link'] ?? null;
- $value = $report['value'] ?? null;
-
- $reports[] = [
- 'icon' => $toString($report['icon'] ?? null),
- 'info' => $toString(I18n::translate($info, $info)),
- 'label' => $toString(I18n::translate($label, $label)),
- 'link' => $toString(I18n::translate($link, $link)),
- 'theme' => $toString($report['theme'] ?? null),
- 'value' => $toString(I18n::translate($value, $value))
- ];
- }
-
- return $reports;
+ 'stats' => function (): Stats {
+ return $this->stats ??= Stats::from(
+ model: $this->model(),
+ reports: $this->reports(),
+ size: $this->size()
+ );
+ },
+ 'reports' => function (): array {
+ return $this->stats->reports();
+ },
+ 'size' => function (): string {
+ return $this->stats->size();
}
]
];
diff --git a/public/kirby/config/tags.php b/public/kirby/config/tags.php
index 9f98127..05272b6 100644
--- a/public/kirby/config/tags.php
+++ b/public/kirby/config/tags.php
@@ -206,11 +206,8 @@ return [
// if value is a UUID, resolve to page/file model
// and use the URL as value
- if (
- Uuid::is($tag->value, 'page') === true ||
- Uuid::is($tag->value, 'file') === true
- ) {
- $tag->value = Uuid::for($tag->value)->model()?->url();
+ if (Uuid::is($tag->value, ['page', 'file']) === true) {
+ $tag->value = Uuid::for($tag->value)?->toUrl();
}
// if url is empty, throw exception or link to the error page
diff --git a/public/kirby/i18n/translations/en.json b/public/kirby/i18n/translations/en.json
index 7efbc30..49aebef 100644
--- a/public/kirby/i18n/translations/en.json
+++ b/public/kirby/i18n/translations/en.json
@@ -376,6 +376,7 @@
"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.delete.confirm.selected": "Do you really want to delete the selected entries?",
"field.structure.empty": "No entries yet",
"field.users.empty": "No users selected yet",
@@ -474,6 +475,7 @@
"license.code": "Code",
"license.code.help": "You received your license code after the purchase via email. Please copy and paste it here.",
"license.code.label": "Please enter your license code",
+ "license.remove.text": "Removing the license will irreversibly delete the license file from this site. You can then activate this site with a different license key or re-register the same license key if the domain remains the same.
To change the domain associated with the license, please contact the Kirby team. Read more →
",
"license.status.active.info": "Includes new major versions until {date}",
"license.status.active.label": "Valid license",
"license.status.demo.info": "This is a demo installation",
diff --git a/public/kirby/i18n/translations/ko.json b/public/kirby/i18n/translations/ko.json
index e29a4a6..a01a5c9 100644
--- a/public/kirby/i18n/translations/ko.json
+++ b/public/kirby/i18n/translations/ko.json
@@ -22,7 +22,7 @@
"copy.all": "모두 복사",
"copy.success": "복사되었습니다. ({count})",
"copy.success.multiple": "복사되었습니다. ({count})",
- "copy.url": "Copy URL",
+ "copy.url": "URL 복사",
"create": "등록",
"custom": "개인화",
@@ -92,23 +92,23 @@
"error.cache.type.invalid": "캐시 형식(({type})이 올바르지 않습니다.",
- "error.content.lock.delete": "The version is locked and cannot be deleted",
- "error.content.lock.move": "The source version is locked and cannot be moved",
- "error.content.lock.publish": "This version is already published",
- "error.content.lock.replace": "The version is locked and cannot be replaced",
- "error.content.lock.update": "The version is locked and cannot be updated",
+ "error.content.lock.delete": "잠긴 버전은 삭제할 수 없습니다.",
+ "error.content.lock.move": "잠긴 버전은 이동할 수 없습니다.",
+ "error.content.lock.publish": "이미 발행되었습니다.",
+ "error.content.lock.replace": "잠긴 버전은 교체할 수 없습니다.",
+ "error.content.lock.update": "잠긴 버전은 업데이트할 수 없습니다.",
- "error.entries.max.plural": "You must not add more than {max} entries",
- "error.entries.max.singular": "You must not add more than one entry",
- "error.entries.min.plural": "You must add at least {min} entries",
- "error.entries.min.singular": "You must add at least one entry",
- "error.entries.supports": "\"{type}\" field type is not supported for the entries field",
+ "error.entries.max.plural": "엔트리를 {max}개 이상 추가할 수 없습니다.",
+ "error.entries.max.singular": "엔트리를 하나 이상 추가할 수 없습니다.",
+ "error.entries.min.plural": "엔트리를 {min}개 이상 추가하세요.",
+ "error.entries.min.singular": "엔트리를 하나 이상 추가하세요.",
+ "error.entries.supports": "{type} 필드 타입은 지원하지 않습니다.",
"error.entries.validation": "{index}번째 필드({field})에 오류가 있습니다.",
"error.email.preset.notFound": "기본 이메일 주소({name})가 없습니다.",
"error.field.converter.invalid": "컨버터({converter})가 올바르지 않습니다.",
- "error.field.link.options": "Invalid options: {options}",
+ "error.field.link.options": "설정({options})이 올바르지 않습니다.",
"error.field.type.missing": "필드({name}): 필드 타입({type})이 없습니다.",
"error.file.changeName.empty": "이름을 입력하세요.",
@@ -116,7 +116,7 @@
"error.file.changeTemplate.invalid": "파일({id}) 템플릿을 다음 템플릿({template})으로 변경할 수 없습니다. (valid: \"{blueprints}\")",
"error.file.changeTemplate.permission": "파일({id}) 템플릿을 변경할 수 없습니다.",
- "error.file.delete.multiple": "Not all files could be deleted. Try each remaining file individually to see the specific error that prevents deletion.",
+ "error.file.delete.multiple": "모든 파일을 삭제할 수 없습니다. 각 파일을 확인하세요.",
"error.file.duplicate": "파일명이 같은 파일({filename})이 있습니다.",
"error.file.extension.forbidden": "이 확장자({extension})는 업로드할 수 없습니다.",
"error.file.extension.invalid": "확장자({extension})가 올바르지 않습니다.",
@@ -135,7 +135,7 @@
"error.file.name.missing": "파일명을 입력하세요.",
"error.file.notFound": "파일({filename})이 없습니다.",
"error.file.orientation": "이미지의 비율({orientation})을 확인하세요.",
- "error.file.sort.permission": "You are not allowed to change the sorting of \"{filename}\"",
+ "error.file.sort.permission": "파일({filename})을 정렬할 권한이 없습니다.",
"error.file.type.forbidden": "이 형식({type})의 파일을 업로드할 권한이 없습니다.",
"error.file.type.invalid": "파일 형식({type})이 올바르지 않습니다.",
"error.file.undefined": "\ud30c\uc77c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.",
@@ -179,7 +179,7 @@
"error.page.delete": "페이지({slug})를 삭제할 수 없습니다.",
"error.page.delete.confirm": "페이지를 삭제하려면 페이지의 제목을 입력하세요.",
"error.page.delete.hasChildren": "하위 페이지가 있는 페이지는 삭제할 수 없습니다.",
- "error.page.delete.multiple": "Not all pages could be deleted. Try each remaining page individually to see the specific error that prevents deletion.",
+ "error.page.delete.multiple": "모든 페이지를 삭제할 수 없습니다. 각 페이지를 확인하세요.",
"error.page.delete.permission": "페이지({slug})를 삭제할 권한이 없습니다.",
"error.page.draft.duplicate": "고유 주소({slug})가 같은 초안 페이지가 있습니다.",
"error.page.duplicate": "고유 주소({slug})가 같은 페이지가 있습니다.",
@@ -187,7 +187,7 @@
"error.page.move.ancestor": "해당 페이지로 이동할 수 없습니다.",
"error.page.move.directory": "페이지 디렉토리는 이동할 수 없습니다.",
"error.page.move.duplicate": "고유 주소({slug})가 같은 서브 페이지가 있습니다.",
- "error.page.move.noSections": "The page \"{parent}\" cannot be a parent of any page because it lacks any pages sections in its blueprint",
+ "error.page.move.noSections": "부모 페이지({parent})의 블루프린트에 해당 섹션이 없습니다.",
"error.page.move.notFound": "이동된 페이지를 찾을 수 없습니다.",
"error.page.move.permission": "페이지({slug})를 이동할 권한이 없습니다.",
"error.page.move.template": "이 템플릿({template})은 이 페이지({parent})의 서브 페이지로 이동할 수 없습니다.",
@@ -317,9 +317,9 @@
"field.blocks.heading.name": "제목",
"field.blocks.heading.text": "제목",
"field.blocks.heading.placeholder": "제목",
- "field.blocks.figure.back.plain": "Plain",
- "field.blocks.figure.back.pattern.light": "Pattern (light)",
- "field.blocks.figure.back.pattern.dark": "Pattern (dark)",
+ "field.blocks.figure.back.plain": "플레인",
+ "field.blocks.figure.back.pattern.light": "패턴(밝음)",
+ "field.blocks.figure.back.pattern.dark": "패턴(어두움)",
"field.blocks.image.alt": "대체 텍스트",
"field.blocks.image.caption": "캡션",
"field.blocks.image.crop": "자르기",
@@ -360,7 +360,7 @@
"field.entries.empty": "항목이 없습니다.",
"field.files.empty": "선택한 파일이 없습니다.",
- "field.files.empty.single": "No file selected yet",
+ "field.files.empty.single": "선택한 파일이 없습니다.",
"field.layout.change": "레이아웃 변경",
"field.layout.delete": "레이아웃 삭제",
@@ -372,14 +372,14 @@
"field.object.empty": "정보가 없습니다.",
"field.pages.empty": "선택한 페이지가 없습니다.",
- "field.pages.empty.single": "No page selected yet",
+ "field.pages.empty.single": "선택한 페이지가 없습니다.",
"field.structure.delete.confirm": "이 항목을 삭제할까요?",
"field.structure.delete.confirm.all": "모든 항목을 삭제할까요?",
"field.structure.empty": "항목이 없습니다.",
"field.users.empty": "선택한 사용자가 없습니다.",
- "field.users.empty.single": "No user selected yet",
+ "field.users.empty.single": "선택한 사용자가 없습니다.",
"fields.empty": "필드가 없습니다.",
@@ -394,17 +394,17 @@
"file.sort": "순서 변경",
"files": "파일",
- "files.delete.confirm.selected": "Do you really want to delete the selected files? This action cannot be undone.",
+ "files.delete.confirm.selected": "선택한 파일을 삭제할까요?",
"files.empty": "파일이 없습니다.",
"filter": "필터",
- "form.discard": "Discard changes",
- "form.discard.confirm": "Do you really want to discard all your changes?",
- "form.locked": "This content is disabled for you as it is currently edited by another user",
- "form.unsaved": "The current changes have not yet been saved",
- "form.preview": "Preview changes",
- "form.preview.draft": "Preview draft",
+ "form.discard": "저장되지 않은 항목이 있습니다.",
+ "form.discard.confirm": "저장되지 않은 내용을 삭제할까요?",
+ "form.locked": "다른 사용자가 편집 중입니다.",
+ "form.unsaved": "변경 사항이 저장되지 않았습니다.",
+ "form.preview": "변경 사항 미리 보기",
+ "form.preview.draft": "초안 미리 보기",
"hide": "숨기기",
"hour": "시",
@@ -449,12 +449,12 @@
"language.variables.empty": "번역이 없습니다.",
"language.variable.delete.confirm": "변수({key})를 삭제할까요?",
- "language.variable.entries": "Values",
- "language.variable.entries.help": "Each string will be used for its matching count, e.g. three strings will match in order to counts 0, 1, 2 and more. Use the {count} placeholder to insert the actual count.",
+ "language.variable.entries": "값",
+ "language.variable.entries.help": "각 문자열은 해당하는 개수에 맞게 사용됩니다. 예를 들어 세 개의 문자열은 0, 1, 2 및 그 이상의 개수에 순서대로 대응합니다. 실제 개수를 표시하려면 {Count}를 사용하세요.",
"language.variable.key": "키",
- "language.variable.multiple": "Countable?",
- "language.variable.multiple.text": "Use different translation strings",
- "language.variable.multiple.help": "You can use different values depending on a count you pass along with the language variable, allowing you to create dynamic translations, e.g. singular and plural.",
+ "language.variable.multiple": "셀 수 있나요?",
+ "language.variable.multiple.text": "다른 번역 문자열을 사용하세요.",
+ "language.variable.multiple.help": "언어 변수와 함께 전달하는 개수에 따라 다른 값을 사용할 수 있으므로 단수형이나 복수형 같은 동적 번역을 구현할 수 있습니다.",
"language.variable.notFound": "변수를 찾을 수 없습니다.",
"language.variable.value": "값",
@@ -486,8 +486,8 @@
"license.status.missing.bubble": "사이트를 공개합니다.",
"license.status.missing.info": "유효한 라이선스가 없습니다.",
"license.status.missing.label": "라이선스를 활성화하세요.",
- "license.status.unknown.info": "The license status is unknown",
- "license.status.unknown.label": "Unknown",
+ "license.status.unknown.info": "라이선스 상태를 알 수 없습니다.",
+ "license.status.unknown.label": "알 수 없음",
"license.manage": "라이선스 관리",
"license.purchased": "구입했습니다.",
"license.success": "Kirby와 함께해주셔서 감사합니다.",
@@ -500,9 +500,9 @@
"lock.unsaved": "저장되지 않은 항목이 있습니다.",
"lock.unsaved.empty": "모든 페이지를 저장했습니다.",
- "lock.unsaved.files": "Unsaved files",
- "lock.unsaved.pages": "Unsaved pages",
- "lock.unsaved.users": "Unsaved accounts",
+ "lock.unsaved.files": "저장되지 않은 파일이 있습니다.",
+ "lock.unsaved.pages": "저장되지 않은 페이지가 있습니다.",
+ "lock.unsaved.users": "저장되지 않은 계정이 있습니다.",
"lock.isLocked": "사용자({email})의 변경 사항이 저장되지 않았습니다.",
"lock.unlock": "잠금 해제",
"lock.unlock.submit": "사용자({email})의 저장되지 않은 변경 사항을 해제하고 덮어쓰기",
@@ -609,7 +609,7 @@
"page.status.unlisted.description": "오직 URL을 통해 접근할 수 있습니다.",
"pages": "페이지",
- "pages.delete.confirm.selected": "Do you really want to delete the selected pages? This action cannot be undone.",
+ "pages.delete.confirm.selected": "선택한 페이지를 삭제할까요?",
"pages.empty": "페이지가 없습니다.",
"pages.status.draft": "초안",
"pages.status.listed": "발행",
@@ -627,7 +627,7 @@
"prev": "이전",
"preview": "미리 보기",
- "publish": "Publish",
+ "publish": "발행",
"published": "발행",
"remove": "삭제",
@@ -649,9 +649,9 @@
"role.nobody.title": "사용자가 없습니다.",
"save": "\uc800\uc7a5",
- "saved": "Saved",
+ "saved": "저장했습니다.",
"search": "검색",
- "searching": "Searching",
+ "searching": "검색 중",
"search.min": "{min}자 이상 입력하세요.",
"search.all": "모든 결과({count}) 보기",
"search.results.none": "해당하는 결과가 없습니다.",
@@ -684,9 +684,9 @@
"system.issues.git": "/.git 폴더의 권한을 확인하세요.",
"system.issues.https": "HTTPS를 권장합니다.",
"system.issues.kirby": "/kirby 폴더의 권한을 확인하세요.",
- "system.issues.local": "The site is running locally with relaxed security checks",
+ "system.issues.local": "이 사이트는 로컬에서 구동 중입니다.",
"system.issues.site": "/site 폴더의 권한을 확인하세요.",
- "system.issues.vue.compiler": "The Vue template compiler is enabled",
+ "system.issues.vue.compiler": "Vue 템플릿 컴파일러를 활성화했습니다.",
"system.issues.vulnerability.kirby": "설치한 시스템에 취약점이 있습니다.\n심각도: {severity}\n{description}",
"system.issues.vulnerability.plugin": "설치한 플러그인({plugin})에 취약점이 있습니다.\n심각도: {severity}\n{ description }",
"system.updateStatus": "업데이트 상태",
@@ -703,10 +703,10 @@
"tel.placeholder": "+49123456789",
"template": "\ud15c\ud50c\ub9bf",
- "theme": "Theme",
- "theme.light": "Lights on",
- "theme.dark": "Lights off",
- "theme.automatic": "Match system default",
+ "theme": "테마",
+ "theme.light": "밝게",
+ "theme.dark": "어둡게",
+ "theme.automatic": "시스템 기본값과 일치",
"title": "제목",
"today": "오늘",
@@ -766,7 +766,7 @@
"user.changeLanguage": "언어 변경",
"user.changeName": "사용자명 변경",
"user.changePassword": "암호 변경",
- "user.changePassword.current": "Your current password",
+ "user.changePassword.current": "현재 암호",
"user.changePassword.new": "새 암호",
"user.changePassword.new.confirm": "새 암호 확인",
"user.changeRole": "역할 변경",
@@ -778,13 +778,13 @@
"users": "사용자",
"version": "버전",
- "version.changes": "Changed version",
- "version.compare": "Compare versions",
+ "version.changes": "버전 변경",
+ "version.compare": "버전을 교체했습니다.",
"version.current": "현재 버전",
"version.latest": "최신 버전",
"versionInformation": "버전 정보",
- "view": "View",
+ "view": "뷰",
"view.account": "계정",
"view.installation": "\uc124\uce58",
"view.languages": "언어",
diff --git a/public/kirby/i18n/translations/nl.json b/public/kirby/i18n/translations/nl.json
index 1e04eb5..1a51a82 100644
--- a/public/kirby/i18n/translations/nl.json
+++ b/public/kirby/i18n/translations/nl.json
@@ -449,12 +449,12 @@
"language.variables.empty": "Nog geen vertalingen",
"language.variable.delete.confirm": "Weet je zeker dat je de variabele voor {key} wil verwijderen?",
- "language.variable.entries": "Values",
- "language.variable.entries.help": "Each string will be used for its matching count, e.g. three strings will match in order to counts 0, 1, 2 and more. Use the {count} placeholder to insert the actual count.",
+ "language.variable.entries": "Waardes",
+ "language.variable.entries.help": "Elke regel wordt gebruikt voor het bijbehorende aantal, bijvoorbeeld drie regels komen overeen met de aantallen 0, 1, 2 en meer. Gebruik de placeholder {count} om het werkelijke aantal in te voegen.",
"language.variable.key": "Key",
- "language.variable.multiple": "Countable?",
- "language.variable.multiple.text": "Use different translation strings",
- "language.variable.multiple.help": "You can use different values depending on a count you pass along with the language variable, allowing you to create dynamic translations, e.g. singular and plural.",
+ "language.variable.multiple": "Telbaar?",
+ "language.variable.multiple.text": "Gebruik verschillende vertalingen",
+ "language.variable.multiple.help": "Je kan verschillende waarden gebruiken, afhankelijk van een getal dat je samen met de taalvariabele doorgeeft, waardoor je dynamische vertalingen kan maken, bijvoorbeeld enkelvoud en meervoud.",
"language.variable.notFound": "De variabele kan niet gevonden worden",
"language.variable.value": "Waarde",
diff --git a/public/kirby/src/Cms/App.php b/public/kirby/src/Cms/App.php
index 4777671..2896e4d 100644
--- a/public/kirby/src/Cms/App.php
+++ b/public/kirby/src/Cms/App.php
@@ -443,15 +443,15 @@ class App
array $arguments = [],
string $contentType = 'html'
): array {
- $name = basename(strtolower($name));
+ $name = strtolower($name);
$data = [];
// always use the site controller as defaults, if available
- $site = $this->controllerLookup('site', $contentType);
- $site ??= $this->controllerLookup('site');
-
- if ($site !== null) {
- $data = (array)$site->call($this, $arguments);
+ // (unless the controller is a snippet controller)
+ if (strpos($name, '/') === false) {
+ $site = $this->controllerLookup('site', $contentType);
+ $site ??= $this->controllerLookup('site');
+ $data = (array)$site?->call($this, $arguments) ?? [];
}
// try to find a specific representation controller
@@ -460,14 +460,10 @@ class App
// let's try the html controller instead
$controller ??= $this->controllerLookup($name);
- if ($controller !== null) {
- return [
- ...$data,
- ...(array)$controller->call($this, $arguments)
- ];
- }
-
- return $data;
+ return [
+ ...$data,
+ ...(array)$controller?->call($this, $arguments) ?? []
+ ];
}
/**
@@ -482,7 +478,11 @@ class App
}
// controller from site root
- $controller = Controller::load($this->root('controllers') . '/' . $name . '.php', $this->root('controllers'));
+ $controller = Controller::load(
+ file: $this->root('controllers') . '/' . $name . '.php',
+ in: $this->root('controllers')
+ );
+
// controller from extension
$controller ??= $this->extension('controllers', $name);
@@ -580,7 +580,16 @@ class App
$visitor = $this->visitor();
foreach ($visitor->acceptedLanguages() as $acceptedLang) {
- $closure = static function ($language) use ($acceptedLang) {
+ // Find locale matches (e.g. en_GB => en_GB)
+ $matchLocale = function ($language) use ($acceptedLang) {
+ $languageLocale = $language->locale(LC_ALL);
+ $acceptedLocale = $acceptedLang->locale();
+
+ return Str::substr($languageLocale, 0, 5) === Str::substr($acceptedLocale, 0, 5);
+ };
+
+ // Find language matches (e.g. en_GB => en)
+ $matchLanguage = function ($language) use ($acceptedLang) {
$languageLocale = $language->locale(LC_ALL);
$acceptedLocale = $acceptedLang->locale();
@@ -589,7 +598,11 @@ class App
$acceptedLocale === Str::substr($languageLocale, 0, 2);
};
- if ($language = $languages->filter($closure)?->first()) {
+ if ($language = $languages->filter($matchLocale)?->first()) {
+ return $language;
+ }
+
+ if ($language = $languages->filter($matchLanguage)?->first()) {
return $language;
}
}
@@ -768,15 +781,7 @@ class App
// Responses
if ($input instanceof Response) {
- $data = $input->toArray();
-
- // inject headers from the global response configuration
- // lazily (only if they are not already set);
- // the case-insensitive nature of headers will be
- // handled by PHP's `header()` function
- $data['headers'] = [...$response->headers(), ...$data['headers']];
-
- return new Response($data);
+ return $response->send($input);
}
// Pages
diff --git a/public/kirby/src/Cms/AppTranslations.php b/public/kirby/src/Cms/AppTranslations.php
index 2dfe918..c7bdd6a 100644
--- a/public/kirby/src/Cms/AppTranslations.php
+++ b/public/kirby/src/Cms/AppTranslations.php
@@ -88,6 +88,12 @@ trait AppTranslations
*/
public function panelLanguage(): string
{
+ $translation = $this->request()->get('translation');
+
+ if ($translation !== null && $this->translations()->find($translation)) {
+ return $translation;
+ }
+
if ($this->multilang() === true) {
$defaultCode = $this->defaultLanguage()->code();
diff --git a/public/kirby/src/Cms/Core.php b/public/kirby/src/Cms/Core.php
index bd8f051..569189b 100644
--- a/public/kirby/src/Cms/Core.php
+++ b/public/kirby/src/Cms/Core.php
@@ -12,6 +12,7 @@ use Kirby\Cms\Auth\TotpChallenge;
use Kirby\Form\Field\BlocksField;
use Kirby\Form\Field\EntriesField;
use Kirby\Form\Field\LayoutField;
+use Kirby\Form\Field\StatsField;
use Kirby\Panel\Ui\FilePreviews\AudioFilePreview;
use Kirby\Panel\Ui\FilePreviews\ImageFilePreview;
use Kirby\Panel\Ui\FilePreviews\PdfFilePreview;
@@ -270,6 +271,7 @@ class Core
'range' => $this->root . '/fields/range.php',
'select' => $this->root . '/fields/select.php',
'slug' => $this->root . '/fields/slug.php',
+ 'stats' => StatsField::class,
'structure' => $this->root . '/fields/structure.php',
'tags' => $this->root . '/fields/tags.php',
'tel' => $this->root . '/fields/tel.php',
diff --git a/public/kirby/src/Cms/File.php b/public/kirby/src/Cms/File.php
index ebb9969..6bb33d8 100644
--- a/public/kirby/src/Cms/File.php
+++ b/public/kirby/src/Cms/File.php
@@ -31,6 +31,7 @@ use Kirby\Toolkit\Str;
* @license https://getkirby.com/license
*
* @use \Kirby\Cms\HasSiblings<\Kirby\Cms\Files>
+ * @method \Kirby\Uuid\FileUuid uuid()
*/
class File extends ModelWithContent
{
@@ -513,7 +514,7 @@ class File extends ModelWithContent
*/
public function permalink(): string|null
{
- return $this->uuid()?->url();
+ return $this->uuid()?->toPermalink();
}
/**
diff --git a/public/kirby/src/Cms/LanguageVariable.php b/public/kirby/src/Cms/LanguageVariable.php
index 669c07b..935a598 100644
--- a/public/kirby/src/Cms/LanguageVariable.php
+++ b/public/kirby/src/Cms/LanguageVariable.php
@@ -116,6 +116,15 @@ class LanguageVariable
return $this->key;
}
+ /**
+ * Returns the parent language
+ * @since 5.1.0
+ */
+ public function language(): Language
+ {
+ return $this->language;
+ }
+
/**
* Sets a new value for the language variable
*/
diff --git a/public/kirby/src/Cms/License.php b/public/kirby/src/Cms/License.php
index 7685ebb..e005b45 100644
--- a/public/kirby/src/Cms/License.php
+++ b/public/kirby/src/Cms/License.php
@@ -30,6 +30,8 @@ class License
protected const SALT = 'kwAHMLyLPBnHEskzH9pPbJsBxQhKXZnX';
+ protected App $kirby;
+
// cache
protected LicenseStatus $status;
protected LicenseType $type;
@@ -50,6 +52,8 @@ class License
if ($email !== null) {
$this->email = $this->normalizeEmail($email);
}
+
+ $this->kirby = App::instance();
}
/**
@@ -100,6 +104,15 @@ class License
return $this->date !== null ? Str::date(strtotime($this->date), $format, $handler) : null;
}
+ /**
+ * Deletes the license file if it exists
+ * @since 5.1.0
+ */
+ public function delete(): bool
+ {
+ return F::remove($this->root());
+ }
+
/**
* Returns the activation domain if available
*/
@@ -179,7 +192,7 @@ class License
}
// get release date of current major version
- $major = Str::before(App::instance()->version(), '.');
+ $major = Str::before($this->kirby->version(), '.');
$release = strtotime(static::HISTORY[$major] ?? '');
// if there's no matching version in the history
@@ -219,7 +232,7 @@ class License
}
// compare domains
- if ($this->normalizeDomain(App::instance()->system()->indexUrl()) !== $this->normalizeDomain($this->domain)) {
+ if ($this->normalizeDomain($this->kirby->system()->indexUrl()) !== $this->normalizeDomain($this->domain)) {
return false;
}
@@ -236,7 +249,7 @@ class License
}
// get the public key
- $pubKey = F::read(App::instance()->root('kirby') . '/kirby.pub');
+ $pubKey = F::read($this->kirby->root('kirby') . '/kirby.pub');
// verify the license signature
$data = json_encode($this->signatureData());
@@ -328,7 +341,7 @@ class License
public static function read(): static
{
try {
- $license = Json::read(App::instance()->root('license'));
+ $license = Json::read(static::root());
} catch (Throwable) {
return new static();
}
@@ -409,6 +422,15 @@ class License
// @codeCoverageIgnoreEnd
}
+ /**
+ * Returns the root path to the license file
+ * @since 5.1.0
+ */
+ public static function root(): string
+ {
+ return App::instance()->root('license');
+ }
+
/**
* Saves the license in the config folder
*/
@@ -420,11 +442,11 @@ class License
);
}
- // where to store the license file
- $file = App::instance()->root('license');
-
// save the license information
- return Json::write($file, $this->content());
+ return Json::write(
+ file: $this->root(),
+ data: $this->content()
+ );
}
/**
diff --git a/public/kirby/src/Cms/Page.php b/public/kirby/src/Cms/Page.php
index 4c667c7..a764ed1 100644
--- a/public/kirby/src/Cms/Page.php
+++ b/public/kirby/src/Cms/Page.php
@@ -30,6 +30,7 @@ use Throwable;
* @license https://getkirby.com/license
*
* @use \Kirby\Cms\HasSiblings<\Kirby\Cms\Pages>
+ * @method \Kirby\Uuid\PageUuid uuid()
*/
class Page extends ModelWithContent
{
@@ -871,7 +872,7 @@ class Page extends ModelWithContent
*/
public function permalink(): string|null
{
- return $this->uuid()?->url();
+ return $this->uuid()?->toPermalink();
}
/**
diff --git a/public/kirby/src/Cms/PageActions.php b/public/kirby/src/Cms/PageActions.php
index ba959d5..f864616 100644
--- a/public/kirby/src/Cms/PageActions.php
+++ b/public/kirby/src/Cms/PageActions.php
@@ -865,6 +865,9 @@ trait PageActions
'template' => $this->intendedTemplate()->name(),
]);
+ // remove the media directory
+ Dir::remove($this->mediaRoot());
+
// actually do it on disk
if ($this->exists() === true) {
if (Dir::move($this->root(), $page->root()) !== true) {
diff --git a/public/kirby/src/Cms/Responder.php b/public/kirby/src/Cms/Responder.php
index 5bd9802..a6eade0 100644
--- a/public/kirby/src/Cms/Responder.php
+++ b/public/kirby/src/Cms/Responder.php
@@ -4,6 +4,7 @@ namespace Kirby\Cms;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Filesystem\Mime;
+use Kirby\Http\Response as HttpResponse;
use Kirby\Toolkit\Str;
use Stringable;
@@ -337,8 +338,15 @@ class Responder implements Stringable
/**
* Creates and returns the response object from the config
*/
- public function send(string|null $body = null): Response
+ public function send(HttpResponse|string|null $body = null): HttpResponse
{
+ if ($body instanceof HttpResponse) {
+ // inject headers from the responder into the response
+ // (only if they are not already set);
+ $body->setHeaderFallbacks($this->headers());
+ return $body;
+ }
+
if ($body !== null) {
$this->body($body);
}
diff --git a/public/kirby/src/Cms/Site.php b/public/kirby/src/Cms/Site.php
index 6333401..6b5a2a5 100644
--- a/public/kirby/src/Cms/Site.php
+++ b/public/kirby/src/Cms/Site.php
@@ -20,6 +20,8 @@ use Kirby\Toolkit\A;
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://getkirby.com/license
+ *
+ * @method \Kirby\Uuid\SiteUuid uuid()
*/
class Site extends ModelWithContent
{
diff --git a/public/kirby/src/Cms/User.php b/public/kirby/src/Cms/User.php
index 9cd8161..6f749e0 100644
--- a/public/kirby/src/Cms/User.php
+++ b/public/kirby/src/Cms/User.php
@@ -26,6 +26,7 @@ use SensitiveParameter;
* @license https://getkirby.com/license
*
* @use \Kirby\Cms\HasSiblings<\Kirby\Cms\Users>
+ * @method \Kirby\Uuid\UserUuid uuid()
*/
class User extends ModelWithContent
{
diff --git a/public/kirby/src/Filesystem/Mime.php b/public/kirby/src/Filesystem/Mime.php
index 7ddc9ac..aa23c63 100644
--- a/public/kirby/src/Filesystem/Mime.php
+++ b/public/kirby/src/Filesystem/Mime.php
@@ -99,7 +99,7 @@ class Mime
'tgz' => ['application/x-tar', 'application/x-gzip-compressed'],
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
- 'wav' => 'audio/x-wav',
+ 'wav' => ['audio/wav', 'audio/x-wav', 'audio/vnd.wave', 'audio/wave'],
'wbxml' => 'application/wbxml',
'webm' => ['video/webm', 'audio/webm'],
'webp' => 'image/webp',
diff --git a/public/kirby/src/Form/Field/StatsField.php b/public/kirby/src/Form/Field/StatsField.php
new file mode 100644
index 0000000..4d7b8d8
--- /dev/null
+++ b/public/kirby/src/Form/Field/StatsField.php
@@ -0,0 +1,74 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ * @since 5.1.0
+ */
+class StatsField extends FieldClass
+{
+ /**
+ * Array or query string for reports. Each report needs a `label` and `value` and can have additional `info`, `link`, `icon` and `theme` settings.
+ */
+ protected array|string $reports;
+
+ /**
+ * The size of the report cards. Available sizes: `tiny`, `small`, `medium`, `large`
+ */
+ protected string $size;
+
+ /**
+ * Cache for the Stats UI component
+ */
+ protected Stats $stats;
+
+ public function __construct(array $params)
+ {
+ parent::__construct($params);
+
+ $this->reports = $params['reports'] ?? [];
+ $this->size = $params['size'] ?? 'large';
+ }
+
+ public function hasValue(): bool
+ {
+ return false;
+ }
+
+ public function reports(): array
+ {
+ return $this->stats()->reports();
+ }
+
+ public function size(): string
+ {
+ return $this->stats()->size();
+ }
+
+ public function stats(): Stats
+ {
+ return $this->stats ??= Stats::from(
+ model: $this->model,
+ reports: $this->reports,
+ size: $this->size
+ );
+ }
+
+ public function props(): array
+ {
+ return [
+ ...parent::props(),
+ ...$this->stats()->props()
+ ];
+ }
+}
diff --git a/public/kirby/src/Form/FieldClass.php b/public/kirby/src/Form/FieldClass.php
index 1c5d2ef..6844821 100644
--- a/public/kirby/src/Form/FieldClass.php
+++ b/public/kirby/src/Form/FieldClass.php
@@ -4,7 +4,6 @@ namespace Kirby\Form;
use Kirby\Cms\HasSiblings;
use Kirby\Toolkit\I18n;
-use Kirby\Toolkit\Str;
/**
* Abstract field class to be used instead
@@ -22,24 +21,24 @@ use Kirby\Toolkit\Str;
abstract class FieldClass
{
use HasSiblings;
+ use Mixin\After;
use Mixin\Api;
+ use Mixin\Autofocus;
+ use Mixin\Before;
+ use Mixin\Help;
+ use Mixin\Icon;
+ use Mixin\Label;
use Mixin\Model;
+ use Mixin\Placeholder;
use Mixin\Translatable;
use Mixin\Validation;
use Mixin\Value;
use Mixin\When;
+ use Mixin\Width;
- protected string|null $after;
- protected bool $autofocus;
- protected string|null $before;
protected bool $disabled;
- protected string|null $help;
- protected string|null $icon;
- protected string|null $label;
protected string|null $name;
- protected string|null $placeholder;
protected Fields $siblings;
- protected string|null $width;
public function __construct(
protected array $params = []
@@ -75,21 +74,6 @@ abstract class FieldClass
return $this->params[$param] ?? null;
}
- public function after(): string|null
- {
- return $this->stringTemplate($this->after);
- }
-
- public function autofocus(): bool
- {
- return $this->autofocus;
- }
-
- public function before(): string|null
- {
- return $this->stringTemplate($this->before);
- }
-
/**
* Returns optional dialog routes for the field
*/
@@ -114,33 +98,11 @@ abstract class FieldClass
return [];
}
- /**
- * Optional help text below the field
- */
- public function help(): string|null
- {
- if (empty($this->help) === false) {
- $help = $this->stringTemplate($this->help);
- $help = $this->kirby()->kirbytext($help);
- return $help;
- }
-
- return null;
- }
-
protected function i18n(string|array|null $param = null): string|null
{
return empty($param) === false ? I18n::translate($param, $param) : null;
}
- /**
- * Optional icon that will be shown at the end of the field
- */
- public function icon(): string|null
- {
- return $this->icon;
- }
-
public function id(): string
{
return $this->name();
@@ -156,16 +118,6 @@ abstract class FieldClass
return false;
}
- /**
- * The field label can be set as string or associative array with translations
- */
- public function label(): string
- {
- return $this->stringTemplate(
- $this->label ?? Str::ucfirst($this->name())
- );
- }
-
/**
* Returns the field name
*/
@@ -182,14 +134,6 @@ abstract class FieldClass
return $this->params;
}
- /**
- * Optional placeholder value that will be shown when the field is empty
- */
- public function placeholder(): string|null
- {
- return $this->stringTemplate($this->placeholder);
- }
-
/**
* Define the props that will be sent to
* the Vue component
@@ -217,67 +161,21 @@ abstract class FieldClass
];
}
- protected function setAfter(array|string|null $after = null): void
- {
- $this->after = $this->i18n($after);
- }
-
- protected function setAutofocus(bool $autofocus = false): void
- {
- $this->autofocus = $autofocus;
- }
-
- protected function setBefore(array|string|null $before = null): void
- {
- $this->before = $this->i18n($before);
- }
-
protected function setDisabled(bool $disabled = false): void
{
$this->disabled = $disabled;
}
- protected function setHelp(array|string|null $help = null): void
- {
- $this->help = $this->i18n($help);
- }
-
- protected function setIcon(string|null $icon = null): void
- {
- $this->icon = $icon;
- }
-
- protected function setLabel(array|string|null $label = null): void
- {
- $this->label = $this->i18n($label);
- }
-
protected function setName(string|null $name = null): void
{
$this->name = strtolower($name ?? $this->type());
}
- protected function setPlaceholder(array|string|null $placeholder = null): void
- {
- $this->placeholder = $this->i18n($placeholder);
- }
-
protected function setSiblings(Fields|null $siblings = null): void
{
$this->siblings = $siblings ?? new Fields([$this]);
}
- /**
- * Setter for the field width
- */
- protected function setWidth(string|null $width = null): void
- {
- $this->width = $width;
- }
-
- /**
- * Returns all sibling fields for the HasSiblings trait
- */
protected function siblingsCollection(): Fields
{
return $this->siblings;
@@ -314,13 +212,4 @@ abstract class FieldClass
{
return lcfirst(basename(str_replace(['\\', 'Field'], ['/', ''], static::class)));
}
-
- /**
- * Returns the width of the field in
- * the Panel grid
- */
- public function width(): string
- {
- return $this->width ?? '1/1';
- }
}
diff --git a/public/kirby/src/Form/Mixin/After.php b/public/kirby/src/Form/Mixin/After.php
new file mode 100644
index 0000000..3199ddc
--- /dev/null
+++ b/public/kirby/src/Form/Mixin/After.php
@@ -0,0 +1,21 @@
+stringTemplate($this->after);
+ }
+
+ protected function setAfter(array|string|null $after = null): void
+ {
+ $this->after = $this->i18n($after);
+ }
+}
diff --git a/public/kirby/src/Form/Mixin/Autofocus.php b/public/kirby/src/Form/Mixin/Autofocus.php
new file mode 100644
index 0000000..2b57688
--- /dev/null
+++ b/public/kirby/src/Form/Mixin/Autofocus.php
@@ -0,0 +1,21 @@
+autofocus;
+ }
+
+ protected function setAutofocus(bool $autofocus = false): void
+ {
+ $this->autofocus = $autofocus;
+ }
+}
diff --git a/public/kirby/src/Form/Mixin/Before.php b/public/kirby/src/Form/Mixin/Before.php
new file mode 100644
index 0000000..4335a65
--- /dev/null
+++ b/public/kirby/src/Form/Mixin/Before.php
@@ -0,0 +1,21 @@
+stringTemplate($this->before);
+ }
+
+ protected function setBefore(array|string|null $before = null): void
+ {
+ $this->before = $this->i18n($before);
+ }
+}
diff --git a/public/kirby/src/Form/Mixin/EmptyState.php b/public/kirby/src/Form/Mixin/EmptyState.php
index 6f7d72a..a553648 100644
--- a/public/kirby/src/Form/Mixin/EmptyState.php
+++ b/public/kirby/src/Form/Mixin/EmptyState.php
@@ -4,6 +4,9 @@ namespace Kirby\Form\Mixin;
trait EmptyState
{
+ /**
+ * Sets the text for the empty state box
+ */
protected string|null $empty;
protected function setEmpty(string|array|null $empty = null): void
diff --git a/public/kirby/src/Form/Mixin/Help.php b/public/kirby/src/Form/Mixin/Help.php
new file mode 100644
index 0000000..790e9eb
--- /dev/null
+++ b/public/kirby/src/Form/Mixin/Help.php
@@ -0,0 +1,34 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://opensource.org/licenses/MIT
+ */
+trait Help
+{
+ /**
+ * Optional help text below the field
+ */
+ protected string|null $help;
+
+ public function help(): string|null
+ {
+ if (empty($this->help) === false) {
+ $help = $this->stringTemplate($this->help);
+ $help = $this->kirby()->kirbytext($help);
+ return $help;
+ }
+
+ return null;
+ }
+
+ protected function setHelp(array|string|null $help = null): void
+ {
+ $this->help = $this->i18n($help);
+ }
+}
diff --git a/public/kirby/src/Form/Mixin/Icon.php b/public/kirby/src/Form/Mixin/Icon.php
new file mode 100644
index 0000000..b8a5491
--- /dev/null
+++ b/public/kirby/src/Form/Mixin/Icon.php
@@ -0,0 +1,28 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://opensource.org/licenses/MIT
+ */
+trait Icon
+{
+ /**
+ * Optional icon that will be shown at the end of the field
+ */
+ protected string|null $icon;
+
+ public function icon(): string|null
+ {
+ return $this->icon;
+ }
+
+ protected function setIcon(string|null $icon = null): void
+ {
+ $this->icon = $icon;
+ }
+}
diff --git a/public/kirby/src/Form/Mixin/Label.php b/public/kirby/src/Form/Mixin/Label.php
new file mode 100644
index 0000000..3691459
--- /dev/null
+++ b/public/kirby/src/Form/Mixin/Label.php
@@ -0,0 +1,32 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://opensource.org/licenses/MIT
+ */
+trait Label
+{
+ /**
+ * The field label can be set as string or associative array with translations
+ */
+ protected string|null $label;
+
+ public function label(): string|null
+ {
+ return $this->stringTemplate(
+ $this->label ?? Str::ucfirst($this->name())
+ );
+ }
+
+ protected function setLabel(array|string|null $label = null): void
+ {
+ $this->label = $this->i18n($label);
+ }
+}
diff --git a/public/kirby/src/Form/Mixin/Max.php b/public/kirby/src/Form/Mixin/Max.php
index 1641917..07706a3 100644
--- a/public/kirby/src/Form/Mixin/Max.php
+++ b/public/kirby/src/Form/Mixin/Max.php
@@ -4,6 +4,9 @@ namespace Kirby\Form\Mixin;
trait Max
{
+ /**
+ * Sets the maximum number of allowed items in the field
+ */
protected int|null $max;
public function max(): int|null
diff --git a/public/kirby/src/Form/Mixin/Min.php b/public/kirby/src/Form/Mixin/Min.php
index 9f5977e..ba875c4 100644
--- a/public/kirby/src/Form/Mixin/Min.php
+++ b/public/kirby/src/Form/Mixin/Min.php
@@ -4,6 +4,9 @@ namespace Kirby\Form\Mixin;
trait Min
{
+ /**
+ * Sets the minimum number of required items in the field
+ */
protected int|null $min;
public function min(): int|null
diff --git a/public/kirby/src/Form/Mixin/Placeholder.php b/public/kirby/src/Form/Mixin/Placeholder.php
new file mode 100644
index 0000000..0692627
--- /dev/null
+++ b/public/kirby/src/Form/Mixin/Placeholder.php
@@ -0,0 +1,30 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://opensource.org/licenses/MIT
+ */
+trait Placeholder
+{
+ /**
+ * Optional placeholder value that will be shown when the field is empty
+ */
+ protected array|string|null $placeholder;
+
+ public function placeholder(): string|null
+ {
+ return $this->stringTemplate(
+ $this->placeholder
+ );
+ }
+
+ protected function setPlaceholder(array|string|null $placeholder = null): void
+ {
+ $this->placeholder = $this->i18n($placeholder);
+ }
+}
diff --git a/public/kirby/src/Form/Mixin/Translatable.php b/public/kirby/src/Form/Mixin/Translatable.php
index bc36a65..a71a6fd 100644
--- a/public/kirby/src/Form/Mixin/Translatable.php
+++ b/public/kirby/src/Form/Mixin/Translatable.php
@@ -13,6 +13,9 @@ use Kirby\Cms\Language;
*/
trait Translatable
{
+ /**
+ * Should the field be translatable?
+ */
protected bool $translate = true;
/**
@@ -29,17 +32,11 @@ trait Translatable
return true;
}
- /**
- * Set the translatable status
- */
protected function setTranslate(bool $translate = true): void
{
$this->translate = $translate;
}
- /**
- * Should the field be translatable?
- */
public function translate(): bool
{
return $this->translate;
diff --git a/public/kirby/src/Form/Mixin/Validation.php b/public/kirby/src/Form/Mixin/Validation.php
index cd69a0c..58e9ad9 100644
--- a/public/kirby/src/Form/Mixin/Validation.php
+++ b/public/kirby/src/Form/Mixin/Validation.php
@@ -18,6 +18,9 @@ use Kirby\Toolkit\V;
*/
trait Validation
{
+ /**
+ * If `true`, the field has to be filled in correctly to be saved.
+ */
protected bool $required;
/**
@@ -94,9 +97,6 @@ trait Validation
return $this->errors() === [];
}
- /**
- * Getter for the required property
- */
public function required(): bool
{
return $this->required;
diff --git a/public/kirby/src/Form/Mixin/Value.php b/public/kirby/src/Form/Mixin/Value.php
index 3dd3423..5da8567 100644
--- a/public/kirby/src/Form/Mixin/Value.php
+++ b/public/kirby/src/Form/Mixin/Value.php
@@ -13,7 +13,14 @@ use Kirby\Cms\Language;
*/
trait Value
{
+ /**
+ * Default value for the field, which will be used when a page/file/user is created
+ */
protected mixed $default = null;
+
+ /**
+ * The value of the field
+ */
protected mixed $value = null;
/**
diff --git a/public/kirby/src/Form/Mixin/When.php b/public/kirby/src/Form/Mixin/When.php
index 1bc18c9..f0952a6 100644
--- a/public/kirby/src/Form/Mixin/When.php
+++ b/public/kirby/src/Form/Mixin/When.php
@@ -11,6 +11,11 @@ namespace Kirby\Form\Mixin;
*/
trait When
{
+ /**
+ * Conditions when the field will be shown
+ *
+ * @since 3.1.0
+ */
protected array|null $when = null;
/**
@@ -40,17 +45,11 @@ trait When
return true;
}
- /**
- * Setter for the `when` condition
- */
protected function setWhen(array|null $when = null): void
{
$this->when = $when;
}
- /**
- * Returns the `when` condition of the field
- */
public function when(): array|null
{
return $this->when;
diff --git a/public/kirby/src/Form/Mixin/Width.php b/public/kirby/src/Form/Mixin/Width.php
new file mode 100644
index 0000000..6366ea6
--- /dev/null
+++ b/public/kirby/src/Form/Mixin/Width.php
@@ -0,0 +1,29 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://opensource.org/licenses/MIT
+ */
+trait Width
+{
+ /**
+ * The width of the field in the field grid.
+ * Available widths: `1/1`, `1/2`, `1/3`, `1/4`, `2/3`, `3/4`
+ */
+ protected string|null $width;
+
+ protected function setWidth(string|null $width = null): void
+ {
+ $this->width = $width;
+ }
+
+ public function width(): string
+ {
+ return $this->width ?? '1/1';
+ }
+}
diff --git a/public/kirby/src/Http/Cookie.php b/public/kirby/src/Http/Cookie.php
index 468a311..fa0c010 100644
--- a/public/kirby/src/Http/Cookie.php
+++ b/public/kirby/src/Http/Cookie.php
@@ -222,7 +222,6 @@ class Cookie
protected static function trackUsage(string $key): void
{
// lazily request the instance for non-CMS use cases
- $kirby = App::instance(null, true);
- $kirby?->response()->usesCookie($key);
+ App::instance(lazy: true)?->response()->usesCookie($key);
}
}
diff --git a/public/kirby/src/Http/Environment.php b/public/kirby/src/Http/Environment.php
index dd8eee5..1672f75 100644
--- a/public/kirby/src/Http/Environment.php
+++ b/public/kirby/src/Http/Environment.php
@@ -13,13 +13,13 @@ use Kirby\Toolkit\Str;
* secure host and base URL detection, as
* well as loading the dedicated
* environment options.
- * @since 3.7.0
*
* @package Kirby Http
* @author Bastian Allgeier
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
+ * @since 3.7.0
*/
class Environment
{
diff --git a/public/kirby/src/Http/Header.php b/public/kirby/src/Http/Header.php
index 2a7d2cd..bc9d6f6 100644
--- a/public/kirby/src/Http/Header.php
+++ b/public/kirby/src/Http/Header.php
@@ -68,7 +68,7 @@ class Header
$header = 'Content-type: ' . $mime;
- if (empty($charset) === false) {
+ if ($charset !== '') {
$header .= '; charset=' . $charset;
}
diff --git a/public/kirby/src/Http/Params.php b/public/kirby/src/Http/Params.php
index 4a37a81..c762f16 100644
--- a/public/kirby/src/Http/Params.php
+++ b/public/kirby/src/Http/Params.php
@@ -38,7 +38,7 @@ class Params extends Obj implements Stringable
*/
public static function extract(string|array|null $path = null): array
{
- if (empty($path) === true) {
+ if ($path === null || $path === '' || $path === []) {
return [
'path' => null,
'params' => null,
@@ -62,12 +62,16 @@ class Params extends Obj implements Stringable
continue;
}
- $paramParts = Str::split($p, $separator);
- $paramKey = $paramParts[0] ?? null;
- $paramValue = $paramParts[1] ?? null;
+ $parts = Str::split($p, $separator);
- if ($paramKey !== null) {
- $params[rawurldecode($paramKey)] = $paramValue !== null ? rawurldecode($paramValue) : null;
+ if ($key = $parts[0] ?? null) {
+ $key = rawurldecode($key);
+
+ if ($value = $parts[1] ?? null) {
+ $value = rawurldecode($value);
+ }
+
+ $params[$key] = $value;
}
unset($path[$index]);
@@ -89,7 +93,7 @@ class Params extends Obj implements Stringable
public function isEmpty(): bool
{
- return empty((array)$this) === true;
+ return (array)$this === [];
}
public function isNotEmpty(): bool
@@ -97,6 +101,23 @@ class Params extends Obj implements Stringable
return $this->isEmpty() === false;
}
+ /**
+ * Merges the current params with the given params
+ * @since 5.1.0
+ *
+ * @return $this
+ */
+ public function merge(array|string|null $params): static
+ {
+ $params = new static($params);
+
+ foreach ($params as $key => $value) {
+ $this->$key = $value;
+ }
+
+ return $this;
+ }
+
/**
* Returns the param separator according
* to the operating system.
@@ -106,15 +127,7 @@ class Params extends Obj implements Stringable
*/
public static function separator(): string
{
- if (static::$separator !== null) {
- return static::$separator;
- }
-
- if (DIRECTORY_SEPARATOR === '/') {
- return static::$separator = ':';
- }
-
- return static::$separator = ';';
+ return static::$separator ??= DIRECTORY_SEPARATOR === '/' ? ':' : ';';
}
/**
@@ -134,7 +147,9 @@ class Params extends Obj implements Stringable
foreach ($this as $key => $value) {
if ($value !== null && $value !== '') {
- $params[] = rawurlencode($key) . $separator . rawurlencode($value);
+ $key = rawurlencode($key);
+ $value = rawurlencode($value);
+ $params[] = $key . $separator . $value;
}
}
diff --git a/public/kirby/src/Http/Query.php b/public/kirby/src/Http/Query.php
index 9d85657..16300b8 100644
--- a/public/kirby/src/Http/Query.php
+++ b/public/kirby/src/Http/Query.php
@@ -29,7 +29,7 @@ class Query extends Obj implements Stringable
public function isEmpty(): bool
{
- return empty((array)$this) === true;
+ return (array)$this === [];
}
public function isNotEmpty(): bool
@@ -37,11 +37,28 @@ class Query extends Obj implements Stringable
return $this->isEmpty() === false;
}
+ /**
+ * Merges the current query with the given query
+ * @since 5.1.0
+ *
+ * @return $this
+ */
+ public function merge(string|array|null $query): static
+ {
+ $query = new static($query);
+
+ foreach ($query as $key => $value) {
+ $this->$key = $value;
+ }
+
+ return $this;
+ }
+
public function toString(bool $questionMark = false): string
{
$query = http_build_query($this, '', '&', PHP_QUERY_RFC3986);
- if (empty($query) === true) {
+ if ($query === '') {
return '';
}
diff --git a/public/kirby/src/Http/Remote.php b/public/kirby/src/Http/Remote.php
index 6f2733b..b780d7e 100644
--- a/public/kirby/src/Http/Remote.php
+++ b/public/kirby/src/Http/Remote.php
@@ -47,13 +47,14 @@ class Remote
public string $errorMessage;
public array $headers = [];
public array $info = [];
- public array $options = [];
/**
* @throws \Exception when the curl request failed
*/
- public function __construct(string $url, array $options = [])
- {
+ public function __construct(
+ string $url,
+ public array $options = []
+ ) {
$defaults = static::$defaults;
// use the system CA store by default if
@@ -71,11 +72,8 @@ class Remote
$defaults = [...$defaults, ...$app->option('remote', [])];
}
- // set all options
- $this->options = [...$defaults, ...$options];
-
- // add the url
- $this->options['url'] = $url;
+ // set all options, incl. url
+ $this->options = [...$defaults, ...$options, 'url' => $url];
// send the request
$this->fetch();
@@ -277,7 +275,7 @@ class Remote
$query = http_build_query($options['data']);
- if (empty($query) === false) {
+ if ($query !== '') {
$url = match (Url::hasQuery($url)) {
true => $url . '&' . $query,
default => $url . '?' . $query
@@ -339,7 +337,7 @@ class Remote
*/
protected function postfields($data)
{
- if (is_object($data) || is_array($data)) {
+ if (is_object($data) === true || is_array($data) === true) {
return http_build_query($data);
}
diff --git a/public/kirby/src/Http/Request.php b/public/kirby/src/Http/Request.php
index d1e0b61..6f2d2f3 100644
--- a/public/kirby/src/Http/Request.php
+++ b/public/kirby/src/Http/Request.php
@@ -67,12 +67,6 @@ class Request
*/
protected string $method;
- /**
- * All options that have been passed to
- * the request in the constructor
- */
- protected array $options;
-
/**
* The Query object is a wrapper around
* the URL query string, which parses the
@@ -96,9 +90,9 @@ class Request
* data via the $options array or use
* the data from the incoming request.
*/
- public function __construct(array $options = [])
- {
- $this->options = $options;
+ public function __construct(
+ protected array $options = []
+ ) {
$this->method = $this->detectRequestMethod($options['method'] ?? null);
if (isset($options['body']) === true) {
@@ -155,7 +149,7 @@ class Request
}
// lazily request the instance for non-CMS use cases
- $kirby = App::instance(null, true);
+ $kirby = App::instance(lazy: true);
// tell the CMS responder that the response relies on
// the `Authorization` header and its value (even if
@@ -224,13 +218,26 @@ class Request
public function detectRequestMethod(string|null $method = null): string
{
// all possible methods
- $methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'];
+ $methods = [
+ 'CONNECT',
+ 'DELETE',
+ 'GET',
+ 'HEAD',
+ 'OPTIONS',
+ 'PATCH',
+ 'POST',
+ 'PUT',
+ 'TRACE',
+ ];
// the request method can be overwritten with a header
- $methodOverride = strtoupper(Environment::getGlobally('HTTP_X_HTTP_METHOD_OVERRIDE', ''));
+ if ($method === null) {
+ $override = Environment::getGlobally('HTTP_X_HTTP_METHOD_OVERRIDE', '');
+ $override = strtoupper($override);
- if (in_array($methodOverride, $methods, true) === true) {
- $method ??= $methodOverride;
+ if (in_array($override, $methods, true) === true) {
+ $method = $override;
+ }
}
// final chain of options to detect the method
@@ -410,14 +417,15 @@ class Request
// both variants need to be checked separately
// because empty strings are treated as invalid
// but the `??` operator wouldn't do the fallback
-
$option = $this->options['auth'] ?? null;
- if (empty($option) === false) {
+
+ if (is_string($option) === true && $option !== '') {
return $option;
}
$header = $this->header('authorization');
- if (empty($header) === false) {
+
+ if (is_string($header) === true && $header !== '') {
return $header;
}
diff --git a/public/kirby/src/Http/Request/Body.php b/public/kirby/src/Http/Request/Body.php
index ec6f3ce..e9ac19a 100644
--- a/public/kirby/src/Http/Request/Body.php
+++ b/public/kirby/src/Http/Request/Body.php
@@ -20,11 +20,6 @@ class Body implements Stringable
{
use Data;
- /**
- * The raw body content
- */
- protected string|array|null $contents;
-
/**
* The parsed content as array
*/
@@ -36,10 +31,12 @@ class Body implements Stringable
* If null is being passed, the class will
* fetch the body either from the $_POST global
* or from php://input.
+ *
+ * @param array|string|null $contents The raw body content
*/
- public function __construct(array|string|null $contents = null)
- {
- $this->contents = $contents;
+ public function __construct(
+ protected array|string|null $contents = null
+ ) {
}
/**
@@ -52,7 +49,7 @@ class Body implements Stringable
return $this->contents;
}
- if (empty($_POST) === false) {
+ if ($_POST !== []) {
return $this->contents = $_POST;
}
@@ -90,7 +87,7 @@ class Body implements Stringable
// try to parse the body as query string
parse_str($contents, $parsed);
- if (is_array($parsed)) {
+ if (is_array($parsed) === true) {
return $this->data = $parsed;
}
}
diff --git a/public/kirby/src/Http/Request/Data.php b/public/kirby/src/Http/Request/Data.php
index d2f3d95..0a435e1 100644
--- a/public/kirby/src/Http/Request/Data.php
+++ b/public/kirby/src/Http/Request/Data.php
@@ -45,9 +45,11 @@ trait Data
{
if (is_array($key) === true) {
$result = [];
+
foreach ($key as $k) {
$result[$k] = $this->get($k);
}
+
return $result;
}
diff --git a/public/kirby/src/Http/Request/Files.php b/public/kirby/src/Http/Request/Files.php
index 244028a..4cb3dd2 100644
--- a/public/kirby/src/Http/Request/Files.php
+++ b/public/kirby/src/Http/Request/Files.php
@@ -34,7 +34,7 @@ class Files
$files ??= $_FILES;
foreach ($files as $key => $file) {
- if (is_array($file['name'])) {
+ if (is_array($file['name']) === true) {
foreach ($file['name'] as $i => $name) {
$this->files[$key][] = [
'name' => $file['name'][$i] ?? null,
diff --git a/public/kirby/src/Http/Request/Query.php b/public/kirby/src/Http/Request/Query.php
index ac54457..4b7a189 100644
--- a/public/kirby/src/Http/Request/Query.php
+++ b/public/kirby/src/Http/Request/Query.php
@@ -22,7 +22,7 @@ class Query implements Stringable
/**
* The Query data array
*/
- protected array|null $data = null;
+ protected array $data;
/**
* Creates a new Query object.
@@ -56,7 +56,7 @@ class Query implements Stringable
*/
public function isEmpty(): bool
{
- return empty($this->data) === true;
+ return $this->data === [];
}
/**
@@ -64,7 +64,7 @@ class Query implements Stringable
*/
public function isNotEmpty(): bool
{
- return empty($this->data) === false;
+ return $this->data !== [];
}
/**
diff --git a/public/kirby/src/Http/Response.php b/public/kirby/src/Http/Response.php
index 9962151..3b6cdb5 100644
--- a/public/kirby/src/Http/Response.php
+++ b/public/kirby/src/Http/Response.php
@@ -281,8 +281,11 @@ class Response implements Stringable
*
* @since 5.0.3
*/
- public static function refresh(string $location = '/', int $code = 302, int $refresh = 0): static
- {
+ public static function refresh(
+ string $location = '/',
+ int $code = 302,
+ int $refresh = 0
+ ): static {
return new static([
'code' => $code,
'headers' => [
@@ -312,6 +315,19 @@ class Response implements Stringable
return $this->body();
}
+ /**
+ * Sets the provided headers in case they are not already set
+ * @internal
+ * @return $this
+ */
+ public function setHeaderFallbacks(array $headers): static
+ {
+ // the case-insensitive nature of headers will be
+ // handled by PHP's `header()` functions
+ $this->headers = [...$headers, ...$this->headers];
+ return $this;
+ }
+
/**
* Converts all relevant response attributes
* to an associative array for debugging,
diff --git a/public/kirby/src/Http/Route.php b/public/kirby/src/Http/Route.php
index 863908b..97b6da0 100644
--- a/public/kirby/src/Http/Route.php
+++ b/public/kirby/src/Http/Route.php
@@ -13,26 +13,11 @@ use Closure;
*/
class Route
{
- /**
- * The callback action function
- */
- protected Closure $action;
-
/**
* Listed of parsed arguments
*/
protected array $arguments = [];
- /**
- * An array of all passed attributes
- */
- protected array $attributes = [];
-
- /**
- * The registered request method
- */
- protected string $method;
-
/**
* The registered pattern
*/
@@ -74,14 +59,11 @@ class Route
*/
public function __construct(
string $pattern,
- string $method,
- Closure $action,
- array $attributes = []
+ protected string $method,
+ protected Closure $action,
+ protected array $attributes = []
) {
- $this->action = $action;
- $this->attributes = $attributes;
- $this->method = $method;
- $this->pattern = $this->regex(ltrim($pattern, '/'));
+ $this->pattern = $this->regex(ltrim($pattern, '/'));
}
/**
diff --git a/public/kirby/src/Http/Router.php b/public/kirby/src/Http/Router.php
index 5a9be20..1798705 100644
--- a/public/kirby/src/Http/Router.php
+++ b/public/kirby/src/Http/Router.php
@@ -158,6 +158,9 @@ class Router
* The Route's arguments method is used to
* find matches and return all the found
* arguments in the path.
+ *
+ * @param array|null $ignore (Passing null has been deprecated)
+ * @todo Remove support for `$ignore = null` in v6
*/
public function find(
string $path,
@@ -172,16 +175,14 @@ class Router
}
// remove leading and trailing slashes
- $path = trim($path, '/');
+ $path = trim($path, '/');
+ $ignore ??= [];
foreach ($this->routes[$method] as $route) {
$arguments = $route->parse($route->pattern(), $path);
if ($arguments !== false) {
- if (
- empty($ignore) === true ||
- in_array($route, $ignore, true) === false
- ) {
+ if (in_array($route, $ignore, true) === false) {
return $this->route = $route;
}
}
diff --git a/public/kirby/src/Http/Uri.php b/public/kirby/src/Http/Uri.php
index d640920..aa41a96 100644
--- a/public/kirby/src/Http/Uri.php
+++ b/public/kirby/src/Http/Uri.php
@@ -216,10 +216,10 @@ class Uri implements Stringable
if ($app = App::instance(null, true)) {
$environment = $app->environment();
- } else {
- $environment = new Environment();
}
+ $environment ??= new Environment();
+
return new static($environment->requestUrl(), $props);
}
@@ -230,7 +230,7 @@ class Uri implements Stringable
*/
public function domain(): string|null
{
- if (empty($this->host) === true || $this->host === '/') {
+ if ($this->host === null || $this->host === '' || $this->host === '/') {
return null;
}
@@ -255,7 +255,7 @@ class Uri implements Stringable
public function hasFragment(): bool
{
- return empty($this->fragment) === false;
+ return $this->fragment !== null && $this->fragment !== '';
}
public function hasPath(): bool
@@ -281,8 +281,9 @@ class Uri implements Stringable
*/
public function idn(): static
{
- if (empty($this->host) === false) {
- $this->setHost(Idn::decode($this->host));
+ if ($this->isAbsolute() === true) {
+ $host = Idn::decode($this->host);
+ $this->setHost($host);
}
return $this;
}
@@ -295,10 +296,10 @@ class Uri implements Stringable
{
if ($app = App::instance(null, true)) {
$url = $app->url('index');
- } else {
- $url = (new Environment())->baseUrl();
}
+ $url ??= (new Environment())->baseUrl();
+
return new static($url, $props);
}
@@ -307,7 +308,16 @@ class Uri implements Stringable
*/
public function isAbsolute(): bool
{
- return empty($this->host) === false;
+ return $this->host !== null && $this->host !== '';
+ }
+
+ /**
+ * Returns the fragment after the hash
+ * @since 5.1.0
+ */
+ public function fragment(): string|null
+ {
+ return $this->fragment;
}
/**
@@ -465,7 +475,7 @@ class Uri implements Stringable
$url = $this->base();
$slash = true;
- if (empty($url) === true) {
+ if ($url === null || $url === '') {
$url = '/';
$slash = false;
}
@@ -479,8 +489,8 @@ class Uri implements Stringable
$url .= $path;
$url .= $this->query->toString(true);
- if (empty($this->fragment) === false) {
- $url .= '#' . $this->fragment;
+ if ($this->hasFragment() === true) {
+ $url .= '#' . $this->fragment();
}
return $url;
@@ -494,8 +504,9 @@ class Uri implements Stringable
*/
public function unIdn(): static
{
- if (empty($this->host) === false) {
- $this->setHost(Idn::encode($this->host));
+ if ($this->isAbsolute() === true) {
+ $host = Idn::encode($this->host);
+ $this->setHost($host);
}
return $this;
}
diff --git a/public/kirby/src/Http/Url.php b/public/kirby/src/Http/Url.php
index 930f93d..0f8d697 100644
--- a/public/kirby/src/Http/Url.php
+++ b/public/kirby/src/Http/Url.php
@@ -110,8 +110,10 @@ class Url
/**
* Convert a relative path into an absolute URL
*/
- public static function makeAbsolute(string|null $path = null, string|null $home = null): string
- {
+ public static function makeAbsolute(
+ string|null $path = null,
+ string|null $home = null
+ ): string {
if ($path === '' || $path === '/' || $path === null) {
return $home ?? static::home();
}
@@ -120,7 +122,7 @@ class Url
return $path;
}
- if (static::isAbsolute($path)) {
+ if (static::isAbsolute($path) === true) {
return $path;
}
@@ -128,11 +130,15 @@ class Url
$path = ltrim($path, '/');
$home ??= static::home();
- if (empty($path) === true) {
+ if ($path === '') {
return $home;
}
- return $home === '/' ? '/' . $path : $home . '/' . $path;
+ if ($home === '/') {
+ return '/' . $path;
+ }
+
+ return $home . '/' . $path;
}
/**
diff --git a/public/kirby/src/Http/Visitor.php b/public/kirby/src/Http/Visitor.php
index 1bbdcf6..67350e6 100644
--- a/public/kirby/src/Http/Visitor.php
+++ b/public/kirby/src/Http/Visitor.php
@@ -165,16 +165,16 @@ class Visitor
*/
public function preferredMimeType(string ...$mimeTypes): string|null
{
- foreach ($this->acceptedMimeTypes() as $acceptedMime) {
+ foreach ($this->acceptedMimeTypes() as $accepted) {
// look for direct matches
- if (in_array($acceptedMime->type(), $mimeTypes, true)) {
- return $acceptedMime->type();
+ if (in_array($accepted->type(), $mimeTypes, true) === true) {
+ return $accepted->type();
}
// test each option against wildcard `Accept` values
- foreach ($mimeTypes as $expectedMime) {
- if (Mime::matches($expectedMime, $acceptedMime->type()) === true) {
- return $expectedMime;
+ foreach ($mimeTypes as $expected) {
+ if (Mime::matches($expected, $accepted->type()) === true) {
+ return $expected;
}
}
}
diff --git a/public/kirby/src/Image/Darkroom.php b/public/kirby/src/Image/Darkroom.php
index 3142631..2769c28 100644
--- a/public/kirby/src/Image/Darkroom.php
+++ b/public/kirby/src/Image/Darkroom.php
@@ -5,6 +5,7 @@ namespace Kirby\Image;
use Exception;
use Kirby\Image\Darkroom\GdLib;
use Kirby\Image\Darkroom\ImageMagick;
+use Kirby\Image\Darkroom\Imagick;
/**
* A wrapper around resizing and cropping
@@ -19,8 +20,9 @@ use Kirby\Image\Darkroom\ImageMagick;
class Darkroom
{
public static array $types = [
- 'gd' => GdLib::class,
- 'im' => ImageMagick::class
+ 'gd' => GdLib::class,
+ 'imagick' => Imagick::class,
+ 'im' => ImageMagick::class
];
public function __construct(
@@ -30,19 +32,18 @@ class Darkroom
}
/**
- * Creates a new Darkroom instance for the given
- * type/driver
+ * Creates a new Darkroom instance
+ * for the given type/driver
*
* @throws \Exception
*/
- public static function factory(string $type, array $settings = []): object
+ public static function factory(string $type, array $settings = []): static
{
if (isset(static::$types[$type]) === false) {
throw new Exception(message: 'Invalid Darkroom type');
}
- $class = static::$types[$type];
- return new $class($settings);
+ return new static::$types[$type]($settings);
}
/**
@@ -69,7 +70,12 @@ class Darkroom
*/
protected function options(array $options = []): array
{
- $options = [...$this->settings, ...$options];
+ $options = [
+ ...$this->settings,
+ ...$options,
+ // ensure quality isn't unset by provided options
+ 'quality' => $options['quality'] ?? $this->settings['quality']
+ ];
// normalize the crop option
if ($options['crop'] === true) {
@@ -81,7 +87,7 @@ class Darkroom
$options['blur'] = 10;
}
- // normalize the greyscale option
+ // normalize the grayscale option
if (isset($options['greyscale']) === true) {
$options['grayscale'] = $options['greyscale'];
unset($options['greyscale']);
@@ -98,8 +104,6 @@ class Darkroom
$options['sharpen'] = 50;
}
- $options['quality'] ??= $this->settings['quality'];
-
return $options;
}
diff --git a/public/kirby/src/Image/Darkroom/GdLib.php b/public/kirby/src/Image/Darkroom/GdLib.php
index de94831..049d097 100644
--- a/public/kirby/src/Image/Darkroom/GdLib.php
+++ b/public/kirby/src/Image/Darkroom/GdLib.php
@@ -8,7 +8,7 @@ use Kirby\Image\Darkroom;
use Kirby\Image\Focus;
/**
- * GdLib
+ * GdLib darkroom driver
*
* @package Kirby Image
* @author Bastian Allgeier
diff --git a/public/kirby/src/Image/Darkroom/ImageMagick.php b/public/kirby/src/Image/Darkroom/ImageMagick.php
index c117bb0..bc3f5d9 100644
--- a/public/kirby/src/Image/Darkroom/ImageMagick.php
+++ b/public/kirby/src/Image/Darkroom/ImageMagick.php
@@ -8,13 +8,16 @@ use Kirby\Image\Darkroom;
use Kirby\Image\Focus;
/**
- * ImageMagick
+ * Legacy ImageMagick driver using the convert CLI
*
* @package Kirby Image
* @author Bastian Allgeier
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
+ *
+ * @deprecated 5.1.0 Use `imagick` in the `thumbs.driver` config option instead
+ * @todo Remove in 7.0.0
*/
class ImageMagick extends Darkroom
{
diff --git a/public/kirby/src/Image/Darkroom/Imagick.php b/public/kirby/src/Image/Darkroom/Imagick.php
new file mode 100644
index 0000000..a301754
--- /dev/null
+++ b/public/kirby/src/Image/Darkroom/Imagick.php
@@ -0,0 +1,292 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ */
+class Imagick extends Darkroom
+{
+ protected function autoOrient(Image $image): Image
+ {
+ switch ($image->getImageOrientation()) {
+ case Image::ORIENTATION_TOPLEFT:
+ break;
+ case Image::ORIENTATION_TOPRIGHT:
+ $image->flopImage();
+ break;
+ case Image::ORIENTATION_BOTTOMRIGHT:
+ $image->rotateImage('#000', 180);
+ break;
+ case Image::ORIENTATION_BOTTOMLEFT:
+ $image->flopImage();
+ $image->rotateImage('#000', 180);
+ break;
+ case Image::ORIENTATION_LEFTTOP:
+ $image->flopImage();
+ $image->rotateImage('#000', -90);
+ break;
+ case Image::ORIENTATION_RIGHTTOP:
+ $image->rotateImage('#000', 90);
+ break;
+ case Image::ORIENTATION_RIGHTBOTTOM:
+ $image->flopImage();
+ $image->rotateImage('#000', 90);
+ break;
+ case Image::ORIENTATION_LEFTBOTTOM:
+ $image->rotateImage('#000', -90);
+ break;
+ default: // Invalid orientation
+ break;
+ }
+
+ $image->setImageOrientation(Image::ORIENTATION_TOPLEFT);
+ return $image;
+ }
+
+ /**
+ * Applies the blur settings
+ */
+ protected function blur(Image $image, array $options): Image
+ {
+ if ($options['blur'] !== false) {
+ $image->blurImage(0.0, $options['blur']);
+ }
+
+ return $image;
+ }
+
+ /**
+ * Keep animated gifs
+ */
+ protected function coalesce(Image $image): Image
+ {
+ if ($image->getImageMimeType() === 'image/gif') {
+ return $image->coalesceImages();
+ }
+
+ return $image;
+ }
+
+ /**
+ * Returns additional default parameters for imagemagick
+ */
+ protected function defaults(): array
+ {
+ return parent::defaults() + [
+ 'interlace' => false,
+ 'profiles' => ['icc', 'icm'],
+ 'threads' => 1,
+ ];
+ }
+
+ /**
+ * Applies the correct settings for grayscale images
+ */
+ protected function grayscale(Image $image, array $options): Image
+ {
+ if ($options['grayscale'] === true) {
+ $image->setImageColorspace(Image::COLORSPACE_GRAY);
+ }
+
+ return $image;
+ }
+
+ /**
+ * Applies the correct settings for interlaced JPEGs if
+ * activated via options
+ */
+ protected function interlace(Image $image, array $options): Image
+ {
+ if ($options['interlace'] === true) {
+ $image->setInterlaceScheme(Image::INTERLACE_LINE);
+ }
+
+ return $image;
+ }
+
+ /**
+ * Creates and runs the full imagemagick command
+ * to process the image
+ *
+ * @throws \Exception
+ */
+ public function process(string $file, array $options = []): array
+ {
+ $options = $this->preprocess($file, $options);
+
+ $image = new Image($file);
+ $image = $this->threads($image, $options);
+ $image = $this->interlace($image, $options);
+ $image = $this->coalesce($image);
+ $image = $this->grayscale($image, $options);
+ $image = $this->autoOrient($image);
+ $image = $this->resize($image, $options);
+ $image = $this->quality($image, $options);
+ $image = $this->blur($image, $options);
+ $image = $this->sharpen($image, $options);
+ $image = $this->strip($image, $options);
+
+ if ($this->save($image, $file, $options) === false) {
+ // @codeCoverageIgnoreStart
+ throw new Exception(message: 'The imagemagick result could not be generated');
+ // @codeCoverageIgnoreEnd
+ }
+
+ return $options;
+ }
+
+ /**
+ * Applies the correct JPEG compression quality settings
+ */
+ protected function quality(Image $image, array $options): Image
+ {
+ $image->setImageCompressionQuality($options['quality']);
+ return $image;
+ }
+
+ /**
+ * Creates the correct options to crop or resize the image
+ * and translates the crop positions for imagemagick
+ */
+ protected function resize(Image $image, array $options): Image
+ {
+ // simple resize
+ if ($options['crop'] === false) {
+ $image->thumbnailImage(
+ $options['width'],
+ $options['height'],
+ true
+ );
+
+ return $image;
+ }
+
+ // crop based on focus point
+ if (Focus::isFocalPoint($options['crop']) === true) {
+ if ($focus = Focus::coords(
+ $options['crop'],
+ $options['sourceWidth'],
+ $options['sourceHeight'],
+ $options['width'],
+ $options['height']
+ )) {
+ $image->cropImage(
+ $focus['width'],
+ $focus['height'],
+ $focus['x1'],
+ $focus['y1']
+ );
+
+ $image->thumbnailImage(
+ $options['width'],
+ $options['height'],
+ true
+ );
+
+ return $image;
+ }
+ }
+
+ // translate the gravity option into something imagemagick understands
+ $gravity = match ($options['crop'] ?? null) {
+ 'top left' => Image::GRAVITY_NORTHWEST,
+ 'top' => Image::GRAVITY_NORTH,
+ 'top right' => Image::GRAVITY_NORTHEAST,
+ 'left' => Image::GRAVITY_WEST,
+ 'right' => Image::GRAVITY_EAST,
+ 'bottom left' => Image::GRAVITY_SOUTHWEST,
+ 'bottom' => Image::GRAVITY_SOUTH,
+ 'bottom right' => Image::GRAVITY_SOUTHEAST,
+ default => Image::GRAVITY_CENTER
+ };
+
+ $landscape = $options['width'] >= $options['height'];
+
+ $image->thumbnailImage(
+ $landscape ? $options['width'] : $image->getImageWidth(),
+ $landscape ? $image->getImageHeight() : $options['height'],
+ true
+ );
+
+ $image->setGravity($gravity);
+ $image->cropImage($options['width'], $options['height'], 0, 0);
+
+ return $image;
+ }
+
+ /**
+ * Creates the option for the output file
+ */
+ protected function save(Image $image, string $file, array $options): bool
+ {
+ if ($options['format'] !== null) {
+ $file = pathinfo($file, PATHINFO_DIRNAME) . '/' . pathinfo($file, PATHINFO_FILENAME) . '.' . $options['format'];
+ }
+
+ return $image->writeImages($file, true);
+ }
+
+ /**
+ * Applies sharpening if activated in the options.
+ */
+ protected function sharpen(Image $image, array $options): Image
+ {
+ if (is_int($options['sharpen']) === false) {
+ return $image;
+ }
+
+ $amount = max(1, min(100, $options['sharpen'])) / 100;
+ $image->sharpenImage(0.0, $amount);
+
+ return $image;
+ }
+
+ /**
+ * Removes all metadata but ICC profiles from the image
+ */
+ protected function strip(Image $image, array $options): Image
+ {
+ // strip all profiles but the ICC profile
+ $profiles = $image->getImageProfiles('*', false);
+
+ foreach ($profiles as $profile) {
+ if (in_array($profile, $options['profiles'] ?? [], true) === false) {
+ $image->removeImageProfile($profile);
+ }
+ }
+
+ // strip all properties
+ $properties = $image->getImageProperties('*', false);
+
+ foreach ($properties as $property) {
+ $image->deleteImageProperty($property);
+ }
+
+ return $image;
+ }
+
+ /**
+ * Sets thread limit
+ */
+ protected function threads(Image $image, array $options): Image
+ {
+ $image->setResourceLimit(
+ Image::RESOURCETYPE_THREAD,
+ $options['threads']
+ );
+ return $image;
+ }
+}
diff --git a/public/kirby/src/Image/Exif.php b/public/kirby/src/Image/Exif.php
index ddaaafb..d5d0f0a 100644
--- a/public/kirby/src/Image/Exif.php
+++ b/public/kirby/src/Image/Exif.php
@@ -2,6 +2,7 @@
namespace Kirby\Image;
+use Kirby\Toolkit\A;
use Kirby\Toolkit\V;
/**
@@ -25,7 +26,7 @@ class Exif
protected string|null $exposure = null;
protected string|null $focalLength = null;
protected bool|null $isColor = null;
- protected string|null $iso = null;
+ protected array|string|null $iso = null;
protected Location|null $location = null;
protected string|null $timestamp = null;
protected int $orientation;
@@ -96,6 +97,10 @@ class Exif
*/
public function iso(): string|null
{
+ if (is_array($this->iso) === true) {
+ return A::first($this->iso);
+ }
+
return $this->iso;
}
diff --git a/public/kirby/src/Panel/Collector/FilesCollector.php b/public/kirby/src/Panel/Collector/FilesCollector.php
new file mode 100644
index 0000000..b9e8560
--- /dev/null
+++ b/public/kirby/src/Panel/Collector/FilesCollector.php
@@ -0,0 +1,73 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ */
+class FilesCollector extends ModelsCollector
+{
+ public function __construct(
+ protected bool $flip = false,
+ protected int|null $limit = null,
+ protected int $page = 1,
+ protected Site|Page|User|null $parent = null,
+ protected string|null $query = null,
+ protected string|null $search = null,
+ protected string|null $sortBy = null,
+ protected string|null $template = null,
+ ) {
+ }
+
+ protected function collect(): Files
+ {
+ return $this->parent()->files();
+ }
+
+ protected function collectByQuery(): Files
+ {
+ return $this->parent()->query($this->query, Files::class) ?? new Files([]);
+ }
+
+ protected function filter(Files|Pages|Users $models): Files
+ {
+ return $models->filter(function ($file) {
+ // remove all protected and hidden files
+ if ($file->isListable() === false) {
+ return false;
+ }
+
+ // filter by template
+ if ($this->template !== null && $file->template() !== $this->template) {
+ return false;
+ }
+
+ return true;
+ });
+ }
+
+ public function isSorting(): bool
+ {
+ return true;
+ }
+
+ protected function sort(Files|Pages|Users $models): Files
+ {
+ if ($this->sortBy === null || $this->isSearching() === true) {
+ return $models->sorted();
+ }
+
+ return parent::sort($models);
+ }
+}
diff --git a/public/kirby/src/Panel/Collector/ModelsCollector.php b/public/kirby/src/Panel/Collector/ModelsCollector.php
new file mode 100644
index 0000000..df5969f
--- /dev/null
+++ b/public/kirby/src/Panel/Collector/ModelsCollector.php
@@ -0,0 +1,130 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ */
+abstract class ModelsCollector
+{
+ protected Files|Pages|Users $models;
+ protected Files|Pages|Users $paginated;
+
+ public function __construct(
+ protected int|null $limit = null,
+ protected int $page = 1,
+ protected Site|Page|User|null $parent = null,
+ protected string|null $query = null,
+ protected string|null $search = null,
+ protected string|null $sortBy = null,
+ protected bool $flip = false,
+ ) {
+ }
+
+ abstract protected function collect(): Files|Pages|Users;
+ abstract protected function collectByQuery(): Files|Pages|Users;
+ abstract protected function filter(Files|Pages|Users $models): Files|Pages|Users;
+
+ protected function flip(Files|Pages|Users $models): Files|Pages|Users
+ {
+ return $models->flip();
+ }
+
+ public function isFlipping(): bool
+ {
+ if ($this->isSearching() === true) {
+ return false;
+ }
+
+ return $this->flip === true;
+ }
+
+ public function isQuerying(): bool
+ {
+ return $this->query !== null;
+ }
+
+ public function isSearching(): bool
+ {
+ return $this->search !== null && trim($this->search) !== '';
+ }
+
+ public function isSorting(): bool
+ {
+ if ($this->isSearching() === true) {
+ return false;
+ }
+
+ return $this->sortBy !== null;
+ }
+
+ public function models(bool $paginated = false): Files|Pages|Users
+ {
+ if ($paginated === true) {
+ return $this->paginated ??= $this->models()->paginate([
+ 'limit' => $this->limit ?? 1000,
+ 'page' => $this->page,
+ 'method' => 'none' // the page is manually provided
+ ]);
+ }
+
+ if (isset($this->models) === true) {
+ return $this->models;
+ }
+
+ if ($this->isQuerying() === true) {
+ $models = $this->collectByQuery();
+ } else {
+ $models = $this->collect();
+ }
+
+ $models = $this->filter($models);
+
+ if ($this->isSearching() === true) {
+ $models = $this->search($models);
+ }
+
+ if ($this->isSorting() === true) {
+ $models = $this->sort($models);
+ }
+
+ if ($this->isFlipping() === true) {
+ $models = $this->flip($models);
+ }
+
+ return $this->models ??= $models;
+ }
+
+ public function pagination(): Pagination
+ {
+ return $this->models(paginated: true)->pagination();
+ }
+
+ protected function parent(): Site|Page|User
+ {
+ return $this->parent ?? App::instance()->site();
+ }
+
+ protected function search(Files|Pages|Users $models): Files|Pages|Users
+ {
+ return $models->search($this->search);
+ }
+
+ protected function sort(Files|Pages|Users $models): Files|Pages|Users
+ {
+ return $models->sort(...$models::sortArgs($this->sortBy));
+ }
+}
diff --git a/public/kirby/src/Panel/Collector/PagesCollector.php b/public/kirby/src/Panel/Collector/PagesCollector.php
new file mode 100644
index 0000000..5213446
--- /dev/null
+++ b/public/kirby/src/Panel/Collector/PagesCollector.php
@@ -0,0 +1,85 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ */
+class PagesCollector extends ModelsCollector
+{
+ public function __construct(
+ protected int|null $limit = null,
+ protected int $page = 1,
+ protected Site|Page|User|null $parent = null,
+ protected string|null $query = null,
+ protected string|null $status = null,
+ protected array $templates = [],
+ protected array $templatesIgnore = [],
+ protected string|null $search = null,
+ protected string|null $sortBy = null,
+ protected bool $flip = false,
+ ) {
+ }
+
+ protected function collect(): Pages
+ {
+ return match ($this->status) {
+ 'draft' => $this->parent()->drafts(),
+ 'listed' => $this->parent()->children()->listed(),
+ 'published' => $this->parent()->children(),
+ 'unlisted' => $this->parent()->children()->unlisted(),
+ default => $this->parent()->childrenAndDrafts()
+ };
+ }
+
+ protected function collectByQuery(): Pages
+ {
+ return $this->parent()->query($this->query, Pages::class) ?? new Pages([]);
+ }
+
+ protected function filter(Files|Pages|Users $models): Pages
+ {
+ // filters pages that are protected and not in the templates list
+ // internal `filter()` method used instead of foreach loop that previously included `unset()`
+ // because `unset()` is updating the original data, `filter()` is just filtering
+ // also it has been tested that there is no performance difference
+ // even in 0.1 seconds on 100k virtual pages
+ return $models->filter(function (Page $model): bool {
+ // remove all protected and hidden pages
+ if ($model->isListable() === false) {
+ return false;
+ }
+
+ $intendedTemplate = $model->intendedTemplate()->name();
+
+ // filter by all set templates
+ if (
+ $this->templates &&
+ in_array($intendedTemplate, $this->templates, true) === false
+ ) {
+ return false;
+ }
+
+ // exclude by all ignored templates
+ if (
+ $this->templatesIgnore &&
+ in_array($intendedTemplate, $this->templatesIgnore, true) === true
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+ }
+}
diff --git a/public/kirby/src/Panel/Collector/UsersCollector.php b/public/kirby/src/Panel/Collector/UsersCollector.php
new file mode 100644
index 0000000..5908f11
--- /dev/null
+++ b/public/kirby/src/Panel/Collector/UsersCollector.php
@@ -0,0 +1,62 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ */
+class UsersCollector extends ModelsCollector
+{
+ public function __construct(
+ protected bool $flip = false,
+ protected int|null $limit = null,
+ protected int $page = 1,
+ protected Site|Page|User|null $parent = null,
+ protected string|null $query = null,
+ protected string|null $role = null,
+ protected string|null $search = null,
+ protected string|null $sortBy = null,
+ ) {
+ }
+
+ protected function collect(): Users
+ {
+ return App::instance()->users();
+ }
+
+ protected function collectByQuery(): Users
+ {
+ return $this->parent()->query($this->query, Users::class) ?? new Users([]);
+ }
+
+ protected function filter(Files|Pages|Users $models): Users
+ {
+ $user = App::instance()->user();
+
+ if ($user === null) {
+ return new Users([]);
+ }
+
+ if ($user->role()->permissions()->for('access', 'users') === false) {
+ return new Users([]);
+ }
+
+ if ($this->role !== null) {
+ $models = $models->role($this->role);
+ }
+
+ return $models;
+ }
+}
diff --git a/public/kirby/src/Panel/Controller/Search.php b/public/kirby/src/Panel/Controller/Search.php
index 0baa6da..ce1107b 100644
--- a/public/kirby/src/Panel/Controller/Search.php
+++ b/public/kirby/src/Panel/Controller/Search.php
@@ -3,7 +3,9 @@
namespace Kirby\Panel\Controller;
use Kirby\Cms\App;
-use Kirby\Toolkit\Escape;
+use Kirby\Panel\Ui\Item\FileItem;
+use Kirby\Panel\Ui\Item\PageItem;
+use Kirby\Panel\Ui\Item\UserItem;
/**
* The Search controller takes care of the logic
@@ -40,13 +42,7 @@ class Search
}
return [
- 'results' => $files->values(fn ($file) => [
- 'image' => $file->panel()->image(),
- 'text' => Escape::html($file->filename()),
- 'link' => $file->panel()->url(true),
- 'info' => Escape::html($file->id()),
- 'uuid' => $file->uuid()->toString(),
- ]),
+ 'results' => $files->values(fn ($file) => (new FileItem(file: $file, info: '{{ file.id }}'))->props()),
'pagination' => $files->pagination()?->toArray()
];
}
@@ -67,13 +63,7 @@ class Search
}
return [
- 'results' => $pages->values(fn ($page) => [
- 'image' => $page->panel()->image(),
- 'text' => Escape::html($page->title()->value()),
- 'link' => $page->panel()->url(true),
- 'info' => Escape::html($page->id()),
- 'uuid' => $page->uuid()?->toString(),
- ]),
+ 'results' => $pages->values(fn ($page) => (new PageItem(page: $page, info: '{{ page.id }}'))->props()),
'pagination' => $pages->pagination()?->toArray()
];
}
@@ -91,13 +81,7 @@ class Search
}
return [
- 'results' => $users->values(fn ($user) => [
- 'image' => $user->panel()->image(),
- 'text' => Escape::html($user->username()),
- 'link' => $user->panel()->url(true),
- 'info' => Escape::html($user->role()->title()),
- 'uuid' => $user->uuid()->toString(),
- ]),
+ 'results' => $users->values(fn ($user) => (new UserItem(user: $user))->props()),
'pagination' => $users->pagination()?->toArray()
];
}
diff --git a/public/kirby/src/Panel/Dialog.php b/public/kirby/src/Panel/Dialog.php
index df43c4a..ba59794 100644
--- a/public/kirby/src/Panel/Dialog.php
+++ b/public/kirby/src/Panel/Dialog.php
@@ -50,6 +50,20 @@ class Dialog extends Json
$pattern = trim($prefix . '/' . ($options['pattern'] ?? $id), '/');
$type = str_replace('$', '', static::$key);
+ // create load/submit events from controller class
+ if ($controller = $options['controller'] ?? null) {
+ if (is_string($controller) === true) {
+ if (method_exists($controller, 'for') === true) {
+ $controller = $controller::for(...);
+ } else {
+ $controller = fn (...$args) => new $controller(...$args);
+ }
+ }
+
+ $options['load'] ??= fn (...$args) => $controller(...$args)->load();
+ $options['submit'] ??= fn (...$args) => $controller(...$args)->submit();
+ }
+
// load event
$routes[] = [
'pattern' => $pattern,
diff --git a/public/kirby/src/Panel/File.php b/public/kirby/src/Panel/File.php
index 0fdc574..dab7524 100644
--- a/public/kirby/src/Panel/File.php
+++ b/public/kirby/src/Panel/File.php
@@ -7,6 +7,7 @@ use Kirby\Cms\ModelWithContent;
use Kirby\Filesystem\Asset;
use Kirby\Panel\Ui\Buttons\ViewButtons;
use Kirby\Panel\Ui\FilePreview;
+use Kirby\Panel\Ui\Item\FileItem;
use Kirby\Toolkit\I18n;
use Throwable;
@@ -359,8 +360,9 @@ class File extends Model
*/
public function pickerData(array $params = []): array
{
- $name = $this->model->filename();
- $id = $this->model->id();
+ $name = $this->model->filename();
+ $id = $this->model->id();
+ $absolute = false;
if (empty($params['model']) === false) {
$parent = $this->model->parent();
@@ -374,15 +376,20 @@ class File extends Model
};
}
- $params['text'] ??= '{{ file.filename }}';
+ $item = new FileItem(
+ file: $this->model,
+ dragTextIsAbsolute: $absolute,
+ image: $params['image'] ?? null,
+ info: $params['info'] ?? null,
+ layout: $params['layout'] ?? null,
+ text: $params['text'] ?? null,
+ );
return [
- ...parent::pickerData($params),
- 'dragText' => $this->dragText('auto', absolute: $absolute ?? false),
- 'filename' => $name,
- 'id' => $id,
+ ...$item->props(),
+ 'id' => $id,
+ 'sortable' => true,
'type' => $this->model->type(),
- 'url' => $this->model->url()
];
}
diff --git a/public/kirby/src/Panel/Model.php b/public/kirby/src/Panel/Model.php
index 75a5fc0..7826175 100644
--- a/public/kirby/src/Panel/Model.php
+++ b/public/kirby/src/Panel/Model.php
@@ -9,6 +9,7 @@ use Kirby\Cms\ModelWithContent;
use Kirby\Filesystem\Asset;
use Kirby\Form\Fields;
use Kirby\Http\Uri;
+use Kirby\Panel\Ui\Item\ModelItem;
use Kirby\Toolkit\A;
/**
@@ -338,17 +339,18 @@ abstract class Model
*/
public function pickerData(array $params = []): array
{
+ $item = new ModelItem(
+ model: $this->model,
+ image: $params['image'] ?? null,
+ info: $params['info'] ?? null,
+ layout: $params['layout'] ?? null,
+ text: $params['text'] ?? null,
+ );
+
return [
- 'id' => $this->model->id(),
- 'image' => $this->image(
- $params['image'] ?? [],
- $params['layout'] ?? 'list'
- ),
- 'info' => $this->model->toSafeString($params['info'] ?? false),
- 'link' => $this->url(true),
+ ...$item->props(),
'sortable' => true,
- 'text' => $this->model->toSafeString($params['text'] ?? false),
- 'uuid' => $this->model->uuid()?->toString()
+ 'url' => $this->url(true)
];
}
diff --git a/public/kirby/src/Panel/Page.php b/public/kirby/src/Panel/Page.php
index 269d83f..ca195ea 100644
--- a/public/kirby/src/Panel/Page.php
+++ b/public/kirby/src/Panel/Page.php
@@ -6,6 +6,7 @@ use Kirby\Cms\File as CmsFile;
use Kirby\Cms\ModelWithContent;
use Kirby\Filesystem\Asset;
use Kirby\Panel\Ui\Buttons\ViewButtons;
+use Kirby\Panel\Ui\Item\PageItem;
use Kirby\Toolkit\I18n;
/**
@@ -254,13 +255,18 @@ class Page extends Model
*/
public function pickerData(array $params = []): array
{
- $params['text'] ??= '{{ page.title }}';
+ $item = new PageItem(
+ page: $this->model,
+ image: $params['image'] ?? null,
+ info: $params['info'] ?? null,
+ layout: $params['layout'] ?? null,
+ text: $params['text'] ?? null,
+ );
return [
- ...parent::pickerData($params),
- 'dragText' => $this->dragText(),
+ ...$item->props(),
'hasChildren' => $this->model->hasChildren(),
- 'url' => $this->model->url()
+ 'sortable' => true
];
}
diff --git a/public/kirby/src/Panel/PageCreateDialog.php b/public/kirby/src/Panel/PageCreateDialog.php
index 772f51c..b69dbe4 100644
--- a/public/kirby/src/Panel/PageCreateDialog.php
+++ b/public/kirby/src/Panel/PageCreateDialog.php
@@ -9,10 +9,13 @@ use Kirby\Cms\PageBlueprint;
use Kirby\Cms\PageRules;
use Kirby\Cms\Site;
use Kirby\Cms\User;
+use Kirby\Content\MemoryStorage;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Form\Form;
use Kirby\Toolkit\A;
use Kirby\Toolkit\I18n;
+use Kirby\Uuid\Uuid;
+use Kirby\Uuid\Uuids;
/**
* Manages the Panel dialog to create new pages
@@ -34,6 +37,7 @@ class PageCreateDialog
protected string|null $slug;
protected string|null $template;
protected string|null $title;
+ protected string|null $uuid;
protected Page|Site|User|File $view;
protected string|null $viewId;
@@ -69,6 +73,7 @@ class PageCreateDialog
// optional
string|null $slug = null,
string|null $title = null,
+ string|null $uuid = null,
) {
$this->parentId = $parentId ?? 'site';
$this->parent = Find::parent($this->parentId);
@@ -76,6 +81,7 @@ class PageCreateDialog
$this->slug = $slug;
$this->template = $template;
$this->title = $title;
+ $this->uuid = $uuid;
$this->viewId = $viewId;
$this->view = Find::parent($this->viewId ?? $this->parentId);
}
@@ -139,6 +145,13 @@ class PageCreateDialog
]);
}
+ // pass uuid field to the dialog if uuids are enabled
+ // to use the same uuid and prevent generating a new one
+ // when the page is created
+ if (Uuids::enabled() === true) {
+ $fields['uuid'] = Field::hidden();
+ }
+
return [
...$fields,
'parent' => Field::hidden(),
@@ -154,7 +167,7 @@ class PageCreateDialog
public function customFields(): array
{
$custom = [];
- $ignore = ['title', 'slug', 'parent', 'template'];
+ $ignore = ['title', 'slug', 'parent', 'template', 'uuid'];
$blueprint = $this->blueprint();
$fields = $blueprint->fields();
@@ -255,12 +268,33 @@ class PageCreateDialog
*/
public function model(): Page
{
- return $this->model ??= Page::factory([
+ if (isset($this->model) === true) {
+ return $this->model;
+ }
+
+ $props = [
'slug' => '__new__',
'template' => $this->template,
'model' => $this->template,
'parent' => $this->parent instanceof Page ? $this->parent : null
- ]);
+ ];
+
+ // make sure that a UUID gets generated
+ // and added to content right away
+ if (Uuids::enabled() === true) {
+ $props['content'] = [
+ 'uuid' => $this->uuid = Uuid::generate()
+ ];
+ }
+
+ $this->model = Page::factory($props);
+
+ // change the storage to memory immediately
+ // since this is a temporary model
+ // so that the model does not write to disk
+ $this->model->changeStorage(MemoryStorage::class);
+
+ return $this->model;
}
/**
@@ -294,10 +328,15 @@ class PageCreateDialog
{
$input['title'] ??= $this->title ?? '';
$input['slug'] ??= $this->slug ?? '';
+ $input['uuid'] ??= $this->uuid ?? null;
$input = $this->resolveFieldTemplates($input);
$content = ['title' => trim($input['title'])];
+ if ($uuid = $input['uuid'] ?? null) {
+ $content['uuid'] = $uuid;
+ }
+
foreach ($this->customFields() as $name => $field) {
$content[$name] = $input[$name] ?? null;
}
@@ -377,6 +416,7 @@ class PageCreateDialog
'slug' => $this->slug ?? '',
'template' => $this->template,
'title' => $this->title ?? '',
+ 'uuid' => $this->uuid,
'view' => $this->viewId,
];
diff --git a/public/kirby/src/Panel/Ui/Component.php b/public/kirby/src/Panel/Ui/Component.php
index d271bfd..cb97379 100644
--- a/public/kirby/src/Panel/Ui/Component.php
+++ b/public/kirby/src/Panel/Ui/Component.php
@@ -84,7 +84,10 @@ abstract class Component
return [
'component' => $this->component,
'key' => $this->key(),
- 'props' => array_filter($this->props())
+ 'props' => array_filter(
+ $this->props(),
+ fn ($prop) => $prop !== null
+ )
];
}
}
diff --git a/public/kirby/src/Panel/Ui/Item/FileItem.php b/public/kirby/src/Panel/Ui/Item/FileItem.php
new file mode 100644
index 0000000..cb8e4e1
--- /dev/null
+++ b/public/kirby/src/Panel/Ui/Item/FileItem.php
@@ -0,0 +1,74 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ * @since 5.1.0
+ */
+class FileItem extends ModelItem
+{
+ /**
+ * @var \Kirby\Cms\File
+ */
+ protected ModelWithContent $model;
+
+ /**
+ * @var \Kirby\Panel\File
+ */
+ protected Model $panel;
+
+ public function __construct(
+ File $file,
+ protected bool $dragTextIsAbsolute = false,
+ string|array|false|null $image = [],
+ string|null $info = null,
+ string|null $layout = null,
+ string|null $text = null,
+ ) {
+ parent::__construct(
+ model: $file,
+ image: $image,
+ info: $info,
+ layout: $layout,
+ text: $text ?? '{{ file.filename }}',
+ );
+ }
+
+ protected function dragText(): string
+ {
+ return $this->panel->dragText(absolute: $this->dragTextIsAbsolute);
+ }
+
+ protected function permissions(): array
+ {
+ $permissions = $this->model->permissions();
+
+ return [
+ 'delete' => $permissions->can('delete'),
+ 'sort' => $permissions->can('sort'),
+ ];
+ }
+
+ public function props(): array
+ {
+ return [
+ ...parent::props(),
+ 'dragText' => $this->dragText(),
+ 'extension' => $this->model->extension(),
+ 'filename' => $this->model->filename(),
+ 'mime' => $this->model->mime(),
+ 'parent' => $this->model->parent()->panel()->path(),
+ 'template' => $this->model->template(),
+ 'url' => $this->model->url(),
+ ];
+ }
+}
diff --git a/public/kirby/src/Panel/Ui/Item/ModelItem.php b/public/kirby/src/Panel/Ui/Item/ModelItem.php
new file mode 100644
index 0000000..58914be
--- /dev/null
+++ b/public/kirby/src/Panel/Ui/Item/ModelItem.php
@@ -0,0 +1,74 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ * @since 5.1.0
+ */
+class ModelItem extends Component
+{
+ protected string $layout;
+ protected Panel $panel;
+ protected string $text;
+
+ public function __construct(
+ protected ModelWithContent $model,
+ protected string|array|false|null $image = [],
+ protected string|null $info = null,
+ string|null $layout = null,
+ string|null $text = null,
+ ) {
+ parent::__construct(component: 'k-item');
+
+ $this->layout = $layout ?? 'list';
+ $this->panel = $this->model->panel();
+ $this->text = $text ?? '{{ model.title }}';
+ }
+
+ protected function info(): string|null
+ {
+ return $this->model->toSafeString($this->info ?? false);
+ }
+
+ protected function image(): array|null
+ {
+ return $this->panel->image($this->image, $this->layout);
+ }
+
+ protected function link(): string
+ {
+ return $this->panel->url(true);
+ }
+
+ protected function permissions(): array
+ {
+ return $this->model->permissions()->toArray();
+ }
+
+ public function props(): array
+ {
+ return [
+ 'id' => $this->model->id(),
+ 'image' => $this->image(),
+ 'info' => $this->info(),
+ 'link' => $this->link(),
+ 'permissions' => $this->permissions(),
+ 'text' => $this->text(),
+ 'uuid' => $this->model->uuid()?->toString(),
+ ];
+ }
+
+ protected function text(): string
+ {
+ return $this->model->toSafeString($this->text);
+ }
+}
diff --git a/public/kirby/src/Panel/Ui/Item/PageItem.php b/public/kirby/src/Panel/Ui/Item/PageItem.php
new file mode 100644
index 0000000..a5651d5
--- /dev/null
+++ b/public/kirby/src/Panel/Ui/Item/PageItem.php
@@ -0,0 +1,74 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ * @since 5.1.0
+ */
+class PageItem extends ModelItem
+{
+ /**
+ * @var \Kirby\Cms\Page
+ */
+ protected ModelWithContent $model;
+
+ /**
+ * @var \Kirby\Panel\Page
+ */
+ protected Model $panel;
+
+ public function __construct(
+ Page $page,
+ string|array|false|null $image = [],
+ string|null $info = null,
+ string|null $layout = null,
+ string|null $text = null,
+ ) {
+ parent::__construct(
+ model: $page,
+ image: $image,
+ info: $info,
+ layout: $layout,
+ text: $text ?? '{{ page.title }}',
+ );
+ }
+
+ protected function dragText(): string
+ {
+ return $this->panel->dragText();
+ }
+
+ protected function permissions(): array
+ {
+ $permissions = $this->model->permissions();
+
+ return [
+ 'changeSlug' => $permissions->can('changeSlug'),
+ 'changeStatus' => $permissions->can('changeStatus'),
+ 'changeTitle' => $permissions->can('changeTitle'),
+ 'delete' => $permissions->can('delete'),
+ 'sort' => $permissions->can('sort'),
+ ];
+ }
+
+ public function props(): array
+ {
+ return [
+ ...parent::props(),
+ 'dragText' => $this->dragText(),
+ 'parent' => $this->model->parentId(),
+ 'status' => $this->model->status(),
+ 'template' => $this->model->intendedTemplate()->name(),
+ 'url' => $this->model->url(),
+ ];
+ }
+}
diff --git a/public/kirby/src/Panel/Ui/Item/UserItem.php b/public/kirby/src/Panel/Ui/Item/UserItem.php
new file mode 100644
index 0000000..d6ade9e
--- /dev/null
+++ b/public/kirby/src/Panel/Ui/Item/UserItem.php
@@ -0,0 +1,38 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ * @since 5.1.0
+ */
+class UserItem extends ModelItem
+{
+ /**
+ * @var \Kirby\Cms\User
+ */
+ protected ModelWithContent $model;
+
+ public function __construct(
+ User $user,
+ string|array|false|null $image = [],
+ string|null $info = '{{ user.role.title }}',
+ string|null $layout = null,
+ string|null $text = null,
+ ) {
+ parent::__construct(
+ model: $user,
+ image: $image,
+ info: $info,
+ layout: $layout,
+ text: $text ?? '{{ user.username }}',
+ );
+ }
+}
diff --git a/public/kirby/src/Panel/Ui/Stat.php b/public/kirby/src/Panel/Ui/Stat.php
new file mode 100644
index 0000000..931f1d3
--- /dev/null
+++ b/public/kirby/src/Panel/Ui/Stat.php
@@ -0,0 +1,140 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ * @since 5.1.0
+ */
+class Stat extends Component
+{
+ public function __construct(
+ public array|string $label,
+ public array|string $value,
+ public string $component = 'k-stat',
+ public array|string|null $dialog = null,
+ public array|string|null $drawer = null,
+ public string|null $icon = null,
+ public array|string|null $info = null,
+ public array|string|null $link = null,
+ public ModelWithContent|null $model = null,
+ public string|null $theme = null,
+ ) {
+ }
+
+ public function dialog(): string|null
+ {
+ return $this->stringTemplate(
+ $this->i18n($this->dialog)
+ );
+ }
+
+ public function drawer(): string|null
+ {
+ return $this->stringTemplate(
+ $this->i18n($this->drawer)
+ );
+ }
+
+ /**
+ * @psalm-suppress TooFewArguments
+ */
+ public static function from(
+ array|string $input,
+ ModelWithContent|null $model = null,
+ ): static {
+ if ($model !== null) {
+ if (is_string($input) === true) {
+ $input = $model->query($input);
+
+ if (is_array($input) === false) {
+ throw new InvalidArgumentException(
+ message: 'Invalid data from stat query. The query must return an array.'
+ );
+ }
+ }
+
+ $input['model'] = $model;
+ }
+
+ return new static(...$input);
+ }
+
+ public function icon(): string|null
+ {
+ return $this->stringTemplate($this->icon);
+ }
+
+ public function info(): string|null
+ {
+ return $this->stringTemplate(
+ $this->i18n($this->info)
+ );
+ }
+
+ public function label(): string
+ {
+ return $this->stringTemplate(
+ $this->i18n($this->label)
+ );
+ }
+
+ public function link(): string|null
+ {
+ return $this->stringTemplate(
+ $this->i18n($this->link)
+ );
+ }
+
+ public function props(): array
+ {
+ return [
+ 'dialog' => $this->dialog(),
+ 'drawer' => $this->drawer(),
+ 'icon' => $this->icon(),
+ 'info' => $this->info(),
+ 'label' => $this->label(),
+ 'link' => $this->link(),
+ 'theme' => $this->theme(),
+ 'value' => $this->value(),
+ ];
+ }
+
+ protected function stringTemplate(string|null $string = null): string|null
+ {
+ if ($this->model === null) {
+ return $string;
+ }
+
+ if ($string !== null) {
+ return $this->model->toString($string);
+ }
+
+ return null;
+ }
+
+ public function theme(): string|null
+ {
+ return $this->stringTemplate($this->theme);
+ }
+
+ protected function i18n(string|array|null $param = null): string|null
+ {
+ return empty($param) === false ? I18n::translate($param, $param) : null;
+ }
+
+ public function value(): string
+ {
+ return $this->stringTemplate(
+ $this->i18n($this->value)
+ );
+ }
+}
diff --git a/public/kirby/src/Panel/Ui/Stats.php b/public/kirby/src/Panel/Ui/Stats.php
new file mode 100644
index 0000000..935bb29
--- /dev/null
+++ b/public/kirby/src/Panel/Ui/Stats.php
@@ -0,0 +1,83 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ * @since 5.1.0
+ */
+class Stats extends Component
+{
+ public function __construct(
+ public string $component = 'k-stats',
+ public ModelWithContent|null $model = null,
+ public array $reports = [],
+ public string $size = 'large',
+ ) {
+ }
+
+ public static function from(
+ ModelWithContent $model,
+ array|string $reports,
+ string $size = 'large'
+ ): static {
+ if (is_string($reports) === true) {
+ $reports = $model->query($reports);
+
+ if (is_array($reports) === false) {
+ throw new InvalidArgumentException(
+ message: 'Invalid data from stats query. The query must return an array.'
+ );
+ }
+ }
+
+ return new static(
+ model: $model,
+ reports: $reports,
+ size: $size
+ );
+ }
+
+ public function props(): array
+ {
+ return [
+ 'reports' => $this->reports(),
+ 'size' => $this->size(),
+ ];
+ }
+
+ public function reports(): array
+ {
+ $reports = [];
+
+ foreach ($this->reports as $stat) {
+ // if not already a Stat object, convert it
+ if ($stat instanceof Stat === false) {
+ try {
+ $stat = Stat::from(
+ input: $stat,
+ model: $this->model
+ );
+ } catch (InvalidArgumentException) {
+ continue;
+ }
+ }
+
+ $reports[] = $stat->props();
+ }
+
+ return $reports;
+ }
+
+ public function size(): string
+ {
+ return $this->size;
+ }
+}
diff --git a/public/kirby/src/Panel/Ui/Upload.php b/public/kirby/src/Panel/Ui/Upload.php
new file mode 100644
index 0000000..a396922
--- /dev/null
+++ b/public/kirby/src/Panel/Ui/Upload.php
@@ -0,0 +1,62 @@
+
+ * @link https://getkirby.com
+ * @copyright Bastian Allgeier
+ * @license https://getkirby.com/license
+ * @since 5.1.0
+ */
+class Upload
+{
+ public function __construct(
+ protected string $api,
+ protected string|null $accept = null,
+ protected array $attributes = [],
+ protected int|null $max = null,
+ protected bool $multiple = true,
+ protected array|bool|null $preview = null,
+ protected int|null $sort = null,
+ protected string|null $template = null,
+ ) {
+ }
+
+ protected function attributes(): array
+ {
+ return [
+ ...$this->attributes,
+ 'sort' => $this->sort,
+ 'template' => $this->template()
+ ];
+ }
+
+ protected function max(): int|null
+ {
+ return $this->multiple() === false ? 1 : $this->max;
+ }
+
+ protected function multiple(): bool
+ {
+ return $this->multiple === true && ($this->max === null || $this->max > 1);
+ }
+
+ public function props(): array
+ {
+ return [
+ 'accept' => $this->accept,
+ 'api' => $this->api,
+ 'attributes' => $this->attributes(),
+ 'max' => $this->max(),
+ 'multiple' => $this->multiple(),
+ 'preview' => $this->preview,
+ ];
+ }
+
+ protected function template(): string|null
+ {
+ return $this->template === 'default' ? null : $this->template;
+ }
+}
diff --git a/public/kirby/src/Panel/User.php b/public/kirby/src/Panel/User.php
index 62df928..1087664 100644
--- a/public/kirby/src/Panel/User.php
+++ b/public/kirby/src/Panel/User.php
@@ -8,6 +8,7 @@ use Kirby\Cms\Translation;
use Kirby\Cms\Url;
use Kirby\Filesystem\Asset;
use Kirby\Panel\Ui\Buttons\ViewButtons;
+use Kirby\Panel\Ui\Item\UserItem;
use Kirby\Toolkit\I18n;
/**
@@ -200,11 +201,18 @@ class User extends Model
*/
public function pickerData(array $params = []): array
{
- $params['text'] ??= '{{ user.username }}';
+ $item = new UserItem(
+ user: $this->model,
+ image: $params['image'] ?? null,
+ info: $params['info'] ?? null,
+ layout: $params['layout'] ?? null,
+ text: $params['text'] ?? null,
+ );
return [
- ...parent::pickerData($params),
+ ...$item->props(),
'email' => $this->model->email(),
+ 'sortable' => true,
'username' => $this->model->username(),
];
}
diff --git a/public/kirby/src/Panel/View.php b/public/kirby/src/Panel/View.php
index 0d51ee7..ab037ec 100644
--- a/public/kirby/src/Panel/View.php
+++ b/public/kirby/src/Panel/View.php
@@ -266,6 +266,7 @@ class View
],
'debug' => $kirby->option('debug', false),
'kirbytext' => $kirby->option('panel.kirbytext', true),
+ 'theme' => $kirby->option('panel.theme', 'system'),
'translation' => $kirby->option('panel.language', 'en'),
'upload' => Upload::chunkSize(),
],
diff --git a/public/kirby/src/Query/AST/ArgumentListNode.php b/public/kirby/src/Query/AST/ArgumentListNode.php
new file mode 100644
index 0000000..b0ed95d
--- /dev/null
+++ b/public/kirby/src/Query/AST/ArgumentListNode.php
@@ -0,0 +1,37 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class ArgumentListNode extends Node
+{
+ public function __construct(
+ public array $arguments = []
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): array|string
+ {
+ // Resolve each argument
+ $arguments = array_map(
+ fn ($argument) => $argument->resolve($visitor),
+ $this->arguments
+ );
+
+ // Keep as array or convert to string
+ // depending on the visitor type
+ return $visitor->arguments($arguments);
+ }
+}
diff --git a/public/kirby/src/Query/AST/ArithmeticNode.php b/public/kirby/src/Query/AST/ArithmeticNode.php
new file mode 100644
index 0000000..18ca20d
--- /dev/null
+++ b/public/kirby/src/Query/AST/ArithmeticNode.php
@@ -0,0 +1,34 @@
+
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class ArithmeticNode extends Node
+{
+ public function __construct(
+ public Node $left,
+ public string $operator,
+ public Node $right
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): mixed
+ {
+ return $visitor->arithmetic(
+ left: $this->left->resolve($visitor),
+ operator: $this->operator,
+ right: $this->right->resolve($visitor)
+ );
+ }
+}
diff --git a/public/kirby/src/Query/AST/ArrayListNode.php b/public/kirby/src/Query/AST/ArrayListNode.php
new file mode 100644
index 0000000..fac7655
--- /dev/null
+++ b/public/kirby/src/Query/AST/ArrayListNode.php
@@ -0,0 +1,37 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class ArrayListNode extends Node
+{
+ public function __construct(
+ public array $elements,
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): array|string
+ {
+ // Resolve each array element
+ $elements = array_map(
+ fn ($element) => $element->resolve($visitor),
+ $this->elements
+ );
+
+ // Keep as array or convert to string
+ // depending on the visitor type
+ return $visitor->arrayList($elements);
+ }
+}
diff --git a/public/kirby/src/Query/AST/ClosureNode.php b/public/kirby/src/Query/AST/ClosureNode.php
new file mode 100644
index 0000000..515bdb7
--- /dev/null
+++ b/public/kirby/src/Query/AST/ClosureNode.php
@@ -0,0 +1,33 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class ClosureNode extends Node
+{
+ /**
+ * @param string[] $arguments The arguments names
+ */
+ public function __construct(
+ public array $arguments,
+ public Node $body,
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): mixed
+ {
+ return $visitor->closure($this);
+ }
+}
diff --git a/public/kirby/src/Query/AST/CoalesceNode.php b/public/kirby/src/Query/AST/CoalesceNode.php
new file mode 100644
index 0000000..af4af6f
--- /dev/null
+++ b/public/kirby/src/Query/AST/CoalesceNode.php
@@ -0,0 +1,33 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class CoalesceNode extends Node
+{
+ public function __construct(
+ public Node $left,
+ public Node $right,
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): mixed
+ {
+ return $visitor->coalescence(
+ left: $this->left->resolve($visitor),
+ right: $this->right->resolve($visitor)
+ );
+ }
+}
diff --git a/public/kirby/src/Query/AST/ComparisonNode.php b/public/kirby/src/Query/AST/ComparisonNode.php
new file mode 100644
index 0000000..9dd6d3e
--- /dev/null
+++ b/public/kirby/src/Query/AST/ComparisonNode.php
@@ -0,0 +1,34 @@
+
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class ComparisonNode extends Node
+{
+ public function __construct(
+ public Node $left,
+ public string $operator,
+ public Node $right
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): bool|string
+ {
+ return $visitor->comparison(
+ left: $this->left->resolve($visitor),
+ operator: $this->operator,
+ right: $this->right->resolve($visitor)
+ );
+ }
+}
diff --git a/public/kirby/src/Query/AST/GlobalFunctionNode.php b/public/kirby/src/Query/AST/GlobalFunctionNode.php
new file mode 100644
index 0000000..7bda18e
--- /dev/null
+++ b/public/kirby/src/Query/AST/GlobalFunctionNode.php
@@ -0,0 +1,33 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class GlobalFunctionNode extends Node
+{
+ public function __construct(
+ public string $name,
+ public ArgumentListNode $arguments,
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): mixed
+ {
+ return $visitor->function(
+ name: $this->name,
+ arguments: $this->arguments->resolve($visitor)
+ );
+ }
+}
diff --git a/public/kirby/src/Query/AST/LiteralNode.php b/public/kirby/src/Query/AST/LiteralNode.php
new file mode 100644
index 0000000..7e861d7
--- /dev/null
+++ b/public/kirby/src/Query/AST/LiteralNode.php
@@ -0,0 +1,29 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class LiteralNode extends Node
+{
+ public function __construct(
+ public mixed $value,
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): mixed
+ {
+ return $visitor->literal($this->value);
+ }
+}
diff --git a/public/kirby/src/Query/AST/LogicalNode.php b/public/kirby/src/Query/AST/LogicalNode.php
new file mode 100644
index 0000000..a7158b5
--- /dev/null
+++ b/public/kirby/src/Query/AST/LogicalNode.php
@@ -0,0 +1,34 @@
+
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class LogicalNode extends Node
+{
+ public function __construct(
+ public Node $left,
+ public string $operator,
+ public Node $right
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): bool|string
+ {
+ return $visitor->logical(
+ left: $this->left->resolve($visitor),
+ operator: $this->operator,
+ right: $this->right->resolve($visitor)
+ );
+ }
+}
diff --git a/public/kirby/src/Query/AST/MemberAccessNode.php b/public/kirby/src/Query/AST/MemberAccessNode.php
new file mode 100644
index 0000000..75f24da
--- /dev/null
+++ b/public/kirby/src/Query/AST/MemberAccessNode.php
@@ -0,0 +1,37 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class MemberAccessNode extends Node
+{
+ public function __construct(
+ public Node $object,
+ public Node $member,
+ public ArgumentListNode|null $arguments = null,
+ public bool $nullSafe = false,
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): mixed
+ {
+ return $visitor->memberAccess(
+ object: $this->object->resolve($visitor),
+ member: $this->member->resolve($visitor),
+ arguments: $this->arguments?->resolve($visitor),
+ nullSafe: $this->nullSafe
+ );
+ }
+}
diff --git a/public/kirby/src/Query/AST/Node.php b/public/kirby/src/Query/AST/Node.php
new file mode 100644
index 0000000..ff5f150
--- /dev/null
+++ b/public/kirby/src/Query/AST/Node.php
@@ -0,0 +1,23 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ *
+ * @codeCoverageIgnore
+ */
+abstract class Node
+{
+ abstract public function resolve(Visitor $visitor);
+}
diff --git a/public/kirby/src/Query/AST/TernaryNode.php b/public/kirby/src/Query/AST/TernaryNode.php
new file mode 100644
index 0000000..a0b8aaa
--- /dev/null
+++ b/public/kirby/src/Query/AST/TernaryNode.php
@@ -0,0 +1,36 @@
+
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class TernaryNode extends Node
+{
+ public function __construct(
+ public Node $condition,
+ public Node $false,
+ public Node|null $true = null
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): mixed
+ {
+ return $visitor->ternary(
+ condition: $this->condition->resolve($visitor),
+ true: $this->true?->resolve($visitor),
+ false: $this->false->resolve($visitor)
+ );
+ }
+}
diff --git a/public/kirby/src/Query/AST/VariableNode.php b/public/kirby/src/Query/AST/VariableNode.php
new file mode 100644
index 0000000..2d5b8ff
--- /dev/null
+++ b/public/kirby/src/Query/AST/VariableNode.php
@@ -0,0 +1,29 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class VariableNode extends Node
+{
+ public function __construct(
+ public string $name,
+ ) {
+ }
+
+ public function resolve(Visitor $visitor): mixed
+ {
+ return $visitor->variable($this->name);
+ }
+}
diff --git a/public/kirby/src/Query/Argument.php b/public/kirby/src/Query/Argument.php
index b213277..2fccef5 100644
--- a/public/kirby/src/Query/Argument.php
+++ b/public/kirby/src/Query/Argument.php
@@ -14,6 +14,8 @@ use Kirby\Toolkit\Str;
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
+ *
+ * @todo Deprecate in v6
*/
class Argument
{
diff --git a/public/kirby/src/Query/Arguments.php b/public/kirby/src/Query/Arguments.php
index 93dd9c3..1659a05 100644
--- a/public/kirby/src/Query/Arguments.php
+++ b/public/kirby/src/Query/Arguments.php
@@ -15,6 +15,8 @@ use Kirby\Toolkit\Collection;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
+ * @todo Deprecate in v6
+ *
* @extends \Kirby\Toolkit\Collection<\Kirby\Query\Argument>
*/
class Arguments extends Collection
diff --git a/public/kirby/src/Query/Expression.php b/public/kirby/src/Query/Expression.php
index 8d2e114..028ec24 100644
--- a/public/kirby/src/Query/Expression.php
+++ b/public/kirby/src/Query/Expression.php
@@ -14,6 +14,8 @@ use Kirby\Toolkit\A;
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
+ *
+ * @todo Deprecate in v6
*/
class Expression
{
diff --git a/public/kirby/src/Query/Parser/Parser.php b/public/kirby/src/Query/Parser/Parser.php
new file mode 100644
index 0000000..96b2651
--- /dev/null
+++ b/public/kirby/src/Query/Parser/Parser.php
@@ -0,0 +1,476 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class Parser
+{
+ protected Token $current;
+ protected Token|null $previous = null;
+
+ /**
+ * @var Iterator
+ */
+ protected Iterator $tokens;
+
+ public function __construct(string|Iterator $query)
+ {
+ if (is_string($query) === true) {
+ $tokenizer = new Tokenizer($query);
+ $query = $tokenizer->tokens();
+ }
+
+ $this->tokens = $query;
+ $this->current = $this->tokens->current();
+ }
+
+ /**
+ * Move to the next token
+ */
+ protected function advance(): Token|null
+ {
+ if ($this->isAtEnd() === false) {
+ $this->previous = $this->current;
+ $this->tokens->next();
+ $this->current = $this->tokens->current();
+ }
+
+ return $this->previous;
+ }
+
+ /**
+ * Parses an array
+ */
+ private function array(): ArrayListNode|null
+ {
+ if ($this->consume(TokenType::T_OPEN_BRACKET)) {
+ return new ArrayListNode(
+ elements: $this->consumeList(TokenType::T_CLOSE_BRACKET)
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses a list of arguments
+ */
+ private function argumentList(): ArgumentListNode
+ {
+ return new ArgumentListNode(
+ arguments: $this->consumeList(TokenType::T_CLOSE_PAREN)
+ );
+ }
+
+ /**
+ * Checks for and parses several atomic expressions
+ */
+ private function atomic(): Node
+ {
+ $token = $this->scalar();
+ $token ??= $this->array();
+ $token ??= $this->identifier();
+ $token ??= $this->grouping();
+
+ if ($token === null) {
+ throw new Exception('Expect expression'); // @codeCoverageIgnore
+ }
+
+ return $token;
+ }
+
+ /**
+ * Checks for and parses a coalesce expression
+ */
+ private function coalesce(): Node
+ {
+ $node = $this->logical();
+
+ while ($this->consume(TokenType::T_COALESCE)) {
+ $node = new CoalesceNode(
+ left: $node,
+ right: $this->logical()
+ );
+ }
+
+ return $node;
+ }
+
+ /**
+ * Collect the next token of a type
+ *
+ * @throws \Exception when next token is not of specified type
+ */
+ protected function consume(
+ TokenType $type,
+ string|false $error = false
+ ): Token|false {
+ if ($this->is($type) === true) {
+ return $this->advance();
+ }
+
+ if (is_string($error) === true) {
+ throw new Exception($error);
+ }
+
+ return false;
+ }
+
+ /**
+ * Move to next token if of any specific type
+ */
+ protected function consumeAny(array $types): Token|false
+ {
+ foreach ($types as $type) {
+ if ($this->is($type) === true) {
+ return $this->advance();
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Collect all list element until closing token
+ */
+ private function consumeList(TokenType $until): array
+ {
+ $elements = [];
+
+ while (
+ $this->isAtEnd() === false &&
+ $this->is($until) === false
+ ) {
+ $elements[] = $this->expression();
+
+ if ($this->consume(TokenType::T_COMMA) === false) {
+ break;
+ }
+ }
+
+ // consume the closing token
+ $this->consume($until, 'Expect closing bracket after list');
+
+ return $elements;
+ }
+
+ /**
+ * Returns the current token
+ */
+ public function current(): Token
+ {
+ return $this->current;
+ }
+
+ /**
+ * Convert a full query expression into a node
+ */
+ private function expression(): Node
+ {
+ // Top-level expression should be ternary
+ return $this->ternary();
+ }
+
+ /**
+ * Parses comparison expressions with proper precedence
+ */
+ private function comparison(): Node
+ {
+ $left = $this->arithmetic();
+
+ while ($token = $this->consumeAny([
+ TokenType::T_EQUAL,
+ TokenType::T_IDENTICAL,
+ TokenType::T_NOT_EQUAL,
+ TokenType::T_NOT_IDENTICAL,
+ TokenType::T_LESS_THAN,
+ TokenType::T_LESS_EQUAL,
+ TokenType::T_GREATER_THAN,
+ TokenType::T_GREATER_EQUAL
+ ])) {
+ $left = new ComparisonNode(
+ left: $left,
+ operator: $token->lexeme,
+ right: $this->arithmetic()
+ );
+ }
+
+ return $left;
+ }
+
+ /**
+ * Parses a grouping (e.g. closure)
+ */
+ private function grouping(): ClosureNode|Node|null
+ {
+ if ($this->consume(TokenType::T_OPEN_PAREN)) {
+ $list = $this->consumeList(TokenType::T_CLOSE_PAREN);
+
+ if ($this->consume(TokenType::T_ARROW)) {
+ $expression = $this->expression();
+
+ /**
+ * Assert that all elements are VariableNodes
+ * @var VariableNode[] $list
+ */
+ foreach ($list as $element) {
+ if ($element instanceof VariableNode === false) {
+ throw new Exception('Expecting only variables in closure argument list');
+ }
+ }
+
+ $arguments = array_map(fn ($element) => $element->name, $list);
+
+ return new ClosureNode(
+ arguments: $arguments,
+ body: $expression
+ );
+ }
+
+ if (count($list) > 1) {
+ throw new Exception('Expecting "=>" after closure argument list');
+ }
+
+ // this is just a grouping
+ return $list[0];
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses an identifier (global functions or variables)
+ */
+ private function identifier(): GlobalFunctionNode|VariableNode|null
+ {
+ if ($token = $this->consume(TokenType::T_IDENTIFIER)) {
+ if ($this->consume(TokenType::T_OPEN_PAREN)) {
+ return new GlobalFunctionNode(
+ name: $token->lexeme,
+ arguments: $this->argumentList()
+ );
+ }
+
+ return new VariableNode(name: $token->lexeme);
+ }
+
+ return null;
+ }
+
+ /**
+ * Whether the current token is of a specific type
+ */
+ protected function is(TokenType $type): bool
+ {
+ if ($this->isAtEnd() === true) {
+ return false;
+ }
+
+ return $this->current->is($type);
+ }
+
+ /**
+ * Whether the parser has reached the end of the query
+ */
+ protected function isAtEnd(): bool
+ {
+ return $this->current->is(TokenType::T_EOF);
+ }
+
+ /**
+ * Checks for and parses a member access expression
+ */
+ private function memberAccess(): Node
+ {
+ $object = $this->atomic();
+
+ while ($token = $this->consumeAny([
+ TokenType::T_DOT,
+ TokenType::T_NULLSAFE,
+ TokenType::T_OPEN_BRACKET
+ ])) {
+ if ($token->is(TokenType::T_OPEN_BRACKET) === true) {
+ // For subscript notation, parse the inside as expression…
+ $member = $this->expression();
+
+ // …and ensure consuming the closing bracket
+ $this->consume(
+ TokenType::T_CLOSE_BRACKET,
+ 'Expect subscript closing bracket'
+ );
+ } elseif ($member = $this->consume(TokenType::T_IDENTIFIER)) {
+ $member = new LiteralNode($member->lexeme);
+ } elseif ($member = $this->consume(TokenType::T_INTEGER)) {
+ $member = new LiteralNode($member->literal);
+ } else {
+ throw new Exception('Expect property name after "."');
+ }
+
+ $object = new MemberAccessNode(
+ object: $object,
+ member: $member,
+ arguments: match ($this->consume(TokenType::T_OPEN_PAREN)) {
+ false => null,
+ default => $this->argumentList(),
+ },
+ nullSafe: $token->is(TokenType::T_NULLSAFE)
+ );
+ }
+
+ return $object;
+ }
+
+ /**
+ * Parses arithmetic expressions with proper precedence
+ */
+ private function arithmetic(): Node
+ {
+ $left = $this->term();
+
+ while ($token = $this->consumeAny([
+ TokenType::T_PLUS,
+ TokenType::T_MINUS
+ ])) {
+ $left = new ArithmeticNode(
+ left: $left,
+ operator: $token->lexeme,
+ right: $this->term()
+ );
+ }
+
+ return $left;
+ }
+
+ /**
+ * Parses multiplication, division, and modulo expressions
+ */
+ private function term(): Node
+ {
+ $left = $this->memberAccess();
+
+ while ($token = $this->consumeAny([
+ TokenType::T_MULTIPLY,
+ TokenType::T_DIVIDE,
+ TokenType::T_MODULO
+ ])) {
+ $left = new ArithmeticNode(
+ left: $left,
+ operator: $token->lexeme,
+ right: $this->memberAccess()
+ );
+ }
+
+ return $left;
+ }
+
+ /**
+ * Parses logical expressions with proper precedence
+ */
+ private function logical(): Node
+ {
+ $left = $this->comparison();
+
+ while ($token = $this->consumeAny([
+ TokenType::T_AND,
+ TokenType::T_OR
+ ])) {
+ $left = new LogicalNode(
+ left: $left,
+ operator: $token->lexeme,
+ right: $this->comparison()
+ );
+ }
+
+ return $left;
+ }
+
+ /**
+ * Parses the tokenized query into AST node tree
+ */
+ public function parse(): Node
+ {
+ // Start parsing chain
+ $expression = $this->expression();
+
+ // Ensure that we consumed all tokens
+ if ($this->isAtEnd() === false) {
+ $this->consume(TokenType::T_EOF, 'Expect end of expression'); // @codeCoverageIgnore
+ }
+
+ return $expression;
+ }
+
+ private function scalar(): LiteralNode|null
+ {
+ if ($token = $this->consumeAny([
+ TokenType::T_TRUE,
+ TokenType::T_FALSE,
+ TokenType::T_NULL,
+ TokenType::T_STRING,
+ TokenType::T_INTEGER,
+ TokenType::T_FLOAT,
+ ])) {
+ return new LiteralNode(value: $token->literal);
+ }
+
+ return null;
+ }
+
+ /**
+ * Checks for and parses a ternary expression
+ * (full `a ? b : c` or elvis shorthand `a ?: c`)
+ */
+ private function ternary(): Node
+ {
+ $condition = $this->coalesce();
+
+ if ($token = $this->consumeAny([
+ TokenType::T_QUESTION_MARK,
+ TokenType::T_TERNARY_DEFAULT
+ ])) {
+ if ($token->is(TokenType::T_TERNARY_DEFAULT) === false) {
+ $true = $this->expression();
+ $this->consume(
+ type: TokenType::T_COLON,
+ error: 'Expect ":" after true branch'
+ );
+ }
+
+ return new TernaryNode(
+ condition: $condition,
+ true: $true ?? null,
+ false: $this->expression()
+ );
+ }
+
+ return $condition;
+ }
+}
diff --git a/public/kirby/src/Query/Parser/Token.php b/public/kirby/src/Query/Parser/Token.php
new file mode 100644
index 0000000..0d7b280
--- /dev/null
+++ b/public/kirby/src/Query/Parser/Token.php
@@ -0,0 +1,30 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class Token
+{
+ public function __construct(
+ public TokenType $type,
+ public string $lexeme,
+ public mixed $literal = null,
+ ) {
+ }
+
+ public function is(TokenType $type): bool
+ {
+ return $this->type === $type;
+ }
+}
diff --git a/public/kirby/src/Query/Parser/TokenType.php b/public/kirby/src/Query/Parser/TokenType.php
new file mode 100644
index 0000000..9ad2b75
--- /dev/null
+++ b/public/kirby/src/Query/Parser/TokenType.php
@@ -0,0 +1,61 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+enum TokenType
+{
+ case T_DOT;
+ case T_COLON;
+ case T_QUESTION_MARK;
+ case T_OPEN_PAREN;
+ case T_CLOSE_PAREN;
+ case T_OPEN_BRACKET;
+ case T_CLOSE_BRACKET;
+ case T_TERNARY_DEFAULT; // ?:
+ case T_NULLSAFE; // ?.
+ case T_COALESCE; // ??
+ case T_COMMA;
+ case T_ARROW;
+ case T_WHITESPACE;
+ case T_EOF;
+
+ // Comparison operators
+ case T_EQUAL; // ==
+ case T_IDENTICAL; // ===
+ case T_NOT_EQUAL; // !=
+ case T_NOT_IDENTICAL; // !==
+ case T_LESS_THAN; // <
+ case T_LESS_EQUAL; // <=
+ case T_GREATER_THAN; // >
+ case T_GREATER_EQUAL; // >=
+
+ // Math operators
+ case T_PLUS; // +
+ case T_MINUS; // -
+ case T_MULTIPLY; // *
+ case T_DIVIDE; // /
+ case T_MODULO; // %
+
+ // Logical operators
+ case T_AND; // AND or &&
+ case T_OR; // OR or ||
+
+ // Literals
+ case T_STRING;
+ case T_INTEGER;
+ case T_FLOAT;
+ case T_TRUE;
+ case T_FALSE;
+ case T_NULL;
+
+ case T_IDENTIFIER;
+}
diff --git a/public/kirby/src/Query/Parser/Tokenizer.php b/public/kirby/src/Query/Parser/Tokenizer.php
new file mode 100644
index 0000000..41f1105
--- /dev/null
+++ b/public/kirby/src/Query/Parser/Tokenizer.php
@@ -0,0 +1,256 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class Tokenizer
+{
+ private int $length = 0;
+
+ /**
+ * The more complex regexes are written here in nowdoc format
+ * so we don't need to double or triple escape backslashes
+ * (that becomes ridiculous rather fast).
+ *
+ * Identifiers can contain letters, numbers and underscores.
+ * They can't start with a number.
+ * For more complex identifier strings, subscript member access
+ * should be used. With `this` to access the global context.
+ */
+ private const IDENTIFIER_REGEX = <<<'REGEX'
+ (?:[\p{L}\p{N}_])*
+ REGEX;
+
+ private const SINGLEQUOTE_STRING_REGEX = <<<'REGEX'
+ '([^'\\]*(?:\\.[^'\\]*)*)'
+ REGEX;
+
+ private const DOUBLEQUOTE_STRING_REGEX = <<<'REGEX'
+ "([^"\\]*(?:\\.[^"\\]*)*)"
+ REGEX;
+
+ public function __construct(
+ private readonly string $query,
+ ) {
+ $this->length = mb_strlen($query);
+ }
+
+ /**
+ * Matches a regex pattern at the current position in the query string.
+ * The matched lexeme will be stored in the $lexeme variable.
+ *
+ * @param int $offset Current position in the query string
+ * @param string $regex Regex pattern without delimiters/flags
+ */
+ public static function match(
+ string $query,
+ int $offset,
+ string $regex,
+ bool $caseInsensitive = false
+ ): string|null {
+ // Add delimiters and flags to the regex
+ $regex = '/\G' . $regex . '/u';
+
+ if ($caseInsensitive === true) {
+ $regex .= 'i';
+ }
+
+ if (preg_match($regex, $query, $matches, 0, $offset) !== 1) {
+ return null;
+ }
+
+ return $matches[0];
+ }
+
+ /**
+ * Scans the source string for a next token
+ * starting from the given position
+ *
+ * @param int $current The current position in the source string
+ *
+ * @throws \Exception If an unexpected character is encountered
+ */
+ public static function token(string $query, int $current): Token
+ {
+ $char = $query[$current];
+
+ // Multi character tokens (check these first):
+ // Whitespace
+ if ($lex = static::match($query, $current, '\s+')) {
+ return new Token(TokenType::T_WHITESPACE, $lex);
+ }
+
+ // true
+ if ($lex = static::match($query, $current, 'true', true)) {
+ return new Token(TokenType::T_TRUE, $lex, true);
+ }
+
+ // false
+ if ($lex = static::match($query, $current, 'false', true)) {
+ return new Token(TokenType::T_FALSE, $lex, false);
+ }
+
+ // null
+ if ($lex = static::match($query, $current, 'null', true)) {
+ return new Token(TokenType::T_NULL, $lex, null);
+ }
+
+ // "string"
+ if ($lex = static::match($query, $current, static::DOUBLEQUOTE_STRING_REGEX)) {
+ return new Token(
+ TokenType::T_STRING,
+ $lex,
+ stripcslashes(substr($lex, 1, -1))
+ );
+ }
+
+ // 'string'
+ if ($lex = static::match($query, $current, static::SINGLEQUOTE_STRING_REGEX)) {
+ return new Token(
+ TokenType::T_STRING,
+ $lex,
+ stripcslashes(substr($lex, 1, -1))
+ );
+ }
+
+ // float (check before single character tokens)
+ $lex = static::match($query, $current, '-?\d+\.\d+\b');
+ if ($lex !== null) {
+ return new Token(TokenType::T_FLOAT, $lex, (float)$lex);
+ }
+
+ // int (check before single character tokens)
+ $lex = static::match($query, $current, '-?\d+\b');
+ if ($lex !== null) {
+ return new Token(TokenType::T_INTEGER, $lex, (int)$lex);
+ }
+
+ // Two character tokens:
+ // ??
+ if ($lex = static::match($query, $current, '\?\?')) {
+ return new Token(TokenType::T_COALESCE, $lex);
+ }
+
+ // ?.
+ if ($lex = static::match($query, $current, '\?\s*\.')) {
+ return new Token(TokenType::T_NULLSAFE, $lex);
+ }
+
+ // ?:
+ if ($lex = static::match($query, $current, '\?\s*:')) {
+ return new Token(TokenType::T_TERNARY_DEFAULT, $lex);
+ }
+
+ // =>
+ if ($lex = static::match($query, $current, '=>')) {
+ return new Token(TokenType::T_ARROW, $lex);
+ }
+
+ // Logical operators (check before comparison operators)
+ if ($lex = static::match($query, $current, '&&|AND')) {
+ return new Token(TokenType::T_AND, $lex);
+ }
+
+ if ($lex = static::match($query, $current, '\|\||OR')) {
+ return new Token(TokenType::T_OR, $lex);
+ }
+
+ // Comparison operators (three characters first, then two, then one)
+ // === (must come before ==)
+ if ($lex = static::match($query, $current, '===')) {
+ return new Token(TokenType::T_IDENTICAL, $lex);
+ }
+
+ // !== (must come before !=)
+ if ($lex = static::match($query, $current, '!==')) {
+ return new Token(TokenType::T_NOT_IDENTICAL, $lex);
+ }
+
+ // <= (must come before <)
+ if ($lex = static::match($query, $current, '<=')) {
+ return new Token(TokenType::T_LESS_EQUAL, $lex);
+ }
+
+ // >= (must come before >)
+ if ($lex = static::match($query, $current, '>=')) {
+ return new Token(TokenType::T_GREATER_EQUAL, $lex);
+ }
+
+ // ==
+ if ($lex = static::match($query, $current, '==')) {
+ return new Token(TokenType::T_EQUAL, $lex);
+ }
+
+ // !=
+ if ($lex = static::match($query, $current, '!=')) {
+ return new Token(TokenType::T_NOT_EQUAL, $lex);
+ }
+
+ // Single character tokens (check these last):
+ $token = match ($char) {
+ '.' => new Token(TokenType::T_DOT, '.'),
+ '(' => new Token(TokenType::T_OPEN_PAREN, '('),
+ ')' => new Token(TokenType::T_CLOSE_PAREN, ')'),
+ '[' => new Token(TokenType::T_OPEN_BRACKET, '['),
+ ']' => new Token(TokenType::T_CLOSE_BRACKET, ']'),
+ ',' => new Token(TokenType::T_COMMA, ','),
+ ':' => new Token(TokenType::T_COLON, ':'),
+ '+' => new Token(TokenType::T_PLUS, '+'),
+ '-' => new Token(TokenType::T_MINUS, '-'),
+ '*' => new Token(TokenType::T_MULTIPLY, '*'),
+ '/' => new Token(TokenType::T_DIVIDE, '/'),
+ '%' => new Token(TokenType::T_MODULO, '%'),
+ '?' => new Token(TokenType::T_QUESTION_MARK, '?'),
+ '<' => new Token(TokenType::T_LESS_THAN, '<'),
+ '>' => new Token(TokenType::T_GREATER_THAN, '>'),
+ default => null
+ };
+
+ if ($token !== null) {
+ return $token;
+ }
+
+ // Identifier
+ if ($lex = static::match($query, $current, static::IDENTIFIER_REGEX)) {
+ return new Token(TokenType::T_IDENTIFIER, $lex);
+ }
+
+ // Unknown token
+ throw new Exception('Invalid character in query: ' . $query[$current]);
+ }
+
+ /**
+ * Tokenizes the query string and returns a generator of tokens.
+ * @return Generator
+ */
+ public function tokens(): Generator
+ {
+ $current = 0;
+
+ while ($current < $this->length) {
+ $token = static::token($this->query, $current);
+
+ // Don't yield whitespace tokens (ignore them)
+ if ($token->type !== TokenType::T_WHITESPACE) {
+ yield $token;
+ }
+
+ $current += mb_strlen($token->lexeme);
+ }
+
+ yield new Token(TokenType::T_EOF, '', null);
+ }
+}
diff --git a/public/kirby/src/Query/Query.php b/public/kirby/src/Query/Query.php
index dc6fca8..01f3183 100644
--- a/public/kirby/src/Query/Query.php
+++ b/public/kirby/src/Query/Query.php
@@ -9,35 +9,29 @@ use Kirby\Cms\File;
use Kirby\Cms\Page;
use Kirby\Cms\Site;
use Kirby\Cms\User;
+use Kirby\Exception\InvalidArgumentException;
use Kirby\Image\QrCode;
+use Kirby\Query\Runners\Runner;
use Kirby\Toolkit\I18n;
/**
- * The Query class can be used to query arrays and objects,
- * including their methods with a very simple string-based syntax.
- *
- * Namespace structure - what handles what:
- * - Query Main interface, direct entries
- * - Expression Simple comparisons (`a ? b :c`)
- * - Segments Chain of method calls (`site.find('notes').url`)
- * - Segment Single method call (`find('notes')`)
- * - Arguments Method call parameters (`'template', '!=', 'note'`)
- * - Argument Single parameter, resolving into actual types
+ * The Query class can be used to run expressions on arrays and objects,
+ * including their methods with a very simple string-based syntax
*
* @package Kirby Query
* @author Bastian Allgeier ,
- * Nico Hoffmann
+ * Nico Hoffmann
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class Query
{
- /**
- * Default data entries
- */
+ public static array $cache = [];
public static array $entries = [];
+ public Runner|string $runner;
+
/**
* Creates a new Query object
*/
@@ -47,6 +41,17 @@ class Query
if ($query !== null) {
$this->query = trim($query);
}
+
+ $this->runner = App::instance()->option('query.runner', 'legacy');
+
+ if ($this->runner !== 'legacy') {
+
+ if (is_subclass_of($this->runner, Runner::class) === false) {
+ throw new InvalidArgumentException("Query runner $this->runner must extend " . Runner::class);
+ }
+
+ $this->runner = $this->runner::for($this);
+ }
}
/**
@@ -71,6 +76,7 @@ class Query
* can be found, otherwise returns null
*
* @throws \Kirby\Exception\BadMethodCallException If an invalid method is accessed by the query
+ * @throws \Kirby\Exception\InvalidArgumentException If an invalid query runner is set in the config option
*/
public function resolve(array|object $data = []): mixed
{
@@ -78,6 +84,24 @@ class Query
return $data;
}
+ // TODO: switch to 'interpreted' as default in v6
+ // TODO: remove in v7
+ // @codeCoverageIgnoreStart
+
+ if ($this->runner === 'legacy') {
+ return $this->resolveLegacy($data);
+ }
+ // @codeCoverageIgnoreEnd
+
+ return $this->runner->run($this->query, (array)$data);
+ }
+
+ /**
+ * @deprecated 5.1.0
+ * @codeCoverageIgnore
+ */
+ private function resolveLegacy(array|object $data = []): mixed
+ {
// merge data with default entries
if (is_array($data) === true) {
$data = [...static::$entries, ...$data];
diff --git a/public/kirby/src/Query/Runners/DefaultRunner.php b/public/kirby/src/Query/Runners/DefaultRunner.php
new file mode 100644
index 0000000..03d9df2
--- /dev/null
+++ b/public/kirby/src/Query/Runners/DefaultRunner.php
@@ -0,0 +1,69 @@
+
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class DefaultRunner extends Runner
+{
+ /**
+ * Creates a runner for the Query
+ */
+ public static function for(Query $query): static
+ {
+ return new static(
+ global: $query::$entries,
+ interceptor: $query->intercept(...),
+ cache: $query::$cache
+ );
+ }
+
+ protected function resolver(string $query): Closure
+ {
+ // Load closure from cache
+ if (isset($this->cache[$query]) === true) {
+ return $this->cache[$query];
+ }
+
+ // Parse query as AST
+ $parser = new Parser($query);
+ $ast = $parser->parse();
+
+ // Cache closure to resolve same query
+ return $this->cache[$query] = fn (array $context) => $ast->resolve(
+ new DefaultVisitor($this->global, $context, $this->interceptor)
+ );
+ }
+
+ /**
+ * Executes a query within a given data context
+ *
+ * @param array $context Optional variables to be passed to the query
+ *
+ * @throws \Exception when query is invalid or executor not callable
+ */
+ public function run(string $query, array $context = []): mixed
+ {
+ // Try resolving query directly from data context or global functions
+ $entry = Scope::get($query, $context, $this->global, false);
+
+ if ($entry !== false) {
+ return $entry;
+ }
+
+ return $this->resolver($query)($context);
+ }
+}
diff --git a/public/kirby/src/Query/Runners/Runner.php b/public/kirby/src/Query/Runners/Runner.php
new file mode 100644
index 0000000..50745ae
--- /dev/null
+++ b/public/kirby/src/Query/Runners/Runner.php
@@ -0,0 +1,43 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+abstract class Runner
+{
+ /**
+ * @param array $global Allowed global function closures
+ */
+ public function __construct(
+ public array $global = [],
+ protected Closure|null $interceptor = null,
+ protected ArrayAccess|array &$cache = [],
+ ) {
+ }
+
+ /**
+ * Creates a runner instance for the Query
+ */
+ abstract public static function for(Query $query): static;
+
+ /**
+ * Executes a query within a given data context
+ *
+ * @param array $context Optional variables to be passed to the query
+ *
+ * @throws \Exception when query is invalid or executor not callable
+ */
+ abstract public function run(string $query, array $context = []): mixed;
+}
diff --git a/public/kirby/src/Query/Runners/Scope.php b/public/kirby/src/Query/Runners/Scope.php
new file mode 100644
index 0000000..6eecf45
--- /dev/null
+++ b/public/kirby/src/Query/Runners/Scope.php
@@ -0,0 +1,94 @@
+
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class Scope
+{
+ /**
+ * Access the key on the object/array during runtime
+ */
+ public static function access(
+ array|object|null $object,
+ string|int $key,
+ bool $nullSafe = false,
+ ...$arguments
+ ): mixed {
+ if ($object === null && $nullSafe === true) {
+ return null;
+ }
+
+ if (is_array($object) === true) {
+ if ($item = $object[$key] ?? $object[(string)$key] ?? null) {
+ if ($arguments) {
+ return $item(...$arguments);
+ }
+
+ if ($item instanceof Closure) {
+ return $item();
+ }
+ }
+
+ return $item;
+ }
+
+ if (is_object($object) === true) {
+ $key = (string)$key;
+
+ if (
+ method_exists($object, $key) === true ||
+ method_exists($object, '__call') === true
+ ) {
+ return $object->$key(...$arguments);
+ }
+
+ return $object->$key ?? null;
+ }
+
+ throw new Exception("Cannot access \"$key\" on " . gettype($object));
+ }
+
+ /**
+ * Resolves a mapping from global context or functions during runtime
+ */
+ public static function get(
+ string $name,
+ array $context = [],
+ array $global = [],
+ false|null $fallback = null
+ ): mixed {
+ // What looks like a variable might actually be a global function
+ // but if there is a variable with the same name,
+ // the variable takes precedence
+ if (isset($context[$name]) === true) {
+ if ($context[$name] instanceof Closure) {
+ return $context[$name]();
+ }
+
+ return $context[$name];
+ }
+
+ if (isset($global[$name]) === true) {
+ return $global[$name]();
+ }
+
+ // Alias to access the global context
+ if ($name === 'this') {
+ return $context;
+ }
+
+ return $fallback;
+ }
+}
diff --git a/public/kirby/src/Query/Segment.php b/public/kirby/src/Query/Segment.php
index 7c24aa9..d1699e2 100644
--- a/public/kirby/src/Query/Segment.php
+++ b/public/kirby/src/Query/Segment.php
@@ -16,6 +16,8 @@ use Kirby\Toolkit\Str;
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
+ *
+ * @todo Deprecate in v6
*/
class Segment
{
diff --git a/public/kirby/src/Query/Segments.php b/public/kirby/src/Query/Segments.php
index 78e83a7..e7a3a49 100644
--- a/public/kirby/src/Query/Segments.php
+++ b/public/kirby/src/Query/Segments.php
@@ -15,6 +15,8 @@ use Kirby\Toolkit\Collection;
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
+ * @todo Deprecate in v6
+ *
* @extends \Kirby\Toolkit\Collection<\Kirby\Query\Segment>
*/
class Segments extends Collection
diff --git a/public/kirby/src/Query/Visitors/DefaultVisitor.php b/public/kirby/src/Query/Visitors/DefaultVisitor.php
new file mode 100644
index 0000000..4f7a96c
--- /dev/null
+++ b/public/kirby/src/Query/Visitors/DefaultVisitor.php
@@ -0,0 +1,188 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ */
+class DefaultVisitor extends Visitor
+{
+ /**
+ * Processes list of arguments
+ */
+ public function arguments(array $arguments): array
+ {
+ return $arguments;
+ }
+
+ /**
+ * Processes arithmetic operation
+ */
+ public function arithmetic(
+ int|float $left,
+ string $operator,
+ int|float $right
+ ): mixed {
+ return match ($operator) {
+ '+' => $left + $right,
+ '-' => $left - $right,
+ '*' => $left * $right,
+ '/' => $left / $right,
+ '%' => $left % $right,
+ default => throw new Exception("Unknown arithmetic operator: $operator")
+ };
+ }
+
+ /**
+ * Processes array
+ */
+ public function arrayList(array $elements): array
+ {
+ return $elements;
+ }
+
+ /**
+ * Processes node into actual closure
+ */
+ public function closure(ClosureNode $node): Closure
+ {
+ $self = $this;
+
+ return function (...$params) use ($self, $node) {
+ // [key1, key2] + [value1, value2] =>
+ // [key1 => value1, key2 => value2]
+ $arguments = array_combine(
+ $node->arguments,
+ $params
+ );
+
+ // Create new nested visitor with combined
+ // data context for resolving the closure body
+ $visitor = new static(
+ global: $self->global,
+ context: [...$self->context, ...$arguments],
+ interceptor: $self->interceptor
+ );
+
+ return $node->body->resolve($visitor);
+ };
+ }
+
+ /**
+ * Processes coalescence operator
+ */
+ public function coalescence(mixed $left, mixed $right): mixed
+ {
+ return $left ?? $right;
+ }
+
+ /**
+ * Processes comparison operation
+ */
+ public function comparison(
+ mixed $left,
+ string $operator,
+ mixed $right
+ ): bool {
+ return match ($operator) {
+ '==' => $left == $right,
+ '===' => $left === $right,
+ '!=' => $left != $right,
+ '!==' => $left !== $right,
+ '<' => $left < $right,
+ '<=' => $left <= $right,
+ '>' => $left > $right,
+ '>=' => $left >= $right,
+ default => throw new Exception("Unknown comparison operator: $operator")
+ };
+ }
+
+ /**
+ * Processes global function
+ */
+ public function function(string $name, array $arguments = []): mixed
+ {
+ $function = $this->global[$name] ?? null;
+
+ if ($function === null) {
+ throw new Exception("Invalid global function in query: $name");
+ }
+
+ return $function(...$arguments);
+ }
+
+ /**
+ * Processes literals
+ */
+ public function literal(mixed $value): mixed
+ {
+ return $value;
+ }
+
+ /**
+ * Processes logical operation
+ */
+ public function logical(
+ mixed $left,
+ string $operator,
+ mixed $right
+ ): bool {
+ return match ($operator) {
+ '&&', 'AND' => $left && $right,
+ '||', 'OR' => $left || $right,
+ default => throw new Exception("Unknown logical operator: $operator")
+ };
+ }
+
+ /**
+ * Processes member access
+ */
+ public function memberAccess(
+ mixed $object,
+ string|int $member,
+ array|null $arguments = null,
+ bool $nullSafe = false
+ ): mixed {
+ if ($this->interceptor !== null) {
+ $object = ($this->interceptor)($object);
+ }
+
+ return Scope::access($object, $member, $nullSafe, ...$arguments ?? []);
+ }
+
+ /**
+ * Processes ternary operator
+ */
+ public function ternary(
+ mixed $condition,
+ mixed $true,
+ mixed $false
+ ): mixed {
+ if ($true === null) {
+ return $condition ?: $false;
+ }
+
+ return $condition ? $true : $false;
+ }
+
+ /**
+ * Get variable from context or global function
+ */
+ public function variable(string $name): mixed
+ {
+ return Scope::get($name, $this->context, $this->global);
+ }
+}
diff --git a/public/kirby/src/Query/Visitors/Visitor.php b/public/kirby/src/Query/Visitors/Visitor.php
new file mode 100644
index 0000000..6425206
--- /dev/null
+++ b/public/kirby/src/Query/Visitors/Visitor.php
@@ -0,0 +1,46 @@
+,
+ * Nico Hoffmann
+ * @link https://getkirby.com
+ * @license https://opensource.org/licenses/MIT
+ * @since 5.1.0
+ * @unstable
+ *
+ * Every visitor class must implement the following methods.
+ * As PHP won't allow increasing the typing specificity, we
+ * aren't actually adding them here in the abstract class, so that
+ * the actual visitor classes can work with much more specific type hints.
+ *
+ * @method mixed arguments(array $arguments)
+ * @method mixed arithmetic(mixed $left, string $operator, mixed $right)
+ * @method mixed arrayList(array $elements)
+ * @method mixed closure($ClosureNode $node))
+ * @method mixed coalescence($left, $right)
+ * @method mixed comparison(mixed $left, string $operator, mixed $right)
+ * @method mixed function($name, $arguments)
+ * @method mixed literal($value)
+ * @method mixed logical(mixed $left, string $operator, mixed $right)
+ * @method mixed memberAccess($object, string|int $member, $arguments, bool $nullSafe = false)
+ * @method mixed ternary($condition, $true, $false)
+ * @method mixed variable(string $name)
+ */
+abstract class Visitor
+{
+ /**
+ * @param array $global valid global function closures
+ * @param array $context data bindings for the query
+ */
+ public function __construct(
+ public array $global = [],
+ public array $context = [],
+ protected Closure|null $interceptor = null
+ ) {
+ }
+}
diff --git a/public/kirby/src/Session/AutoSession.php b/public/kirby/src/Session/AutoSession.php
index 03b7639..225e62a 100644
--- a/public/kirby/src/Session/AutoSession.php
+++ b/public/kirby/src/Session/AutoSession.php
@@ -26,6 +26,7 @@ class AutoSession
* - `durationNormal`: Duration of normal sessions in seconds; defaults to 2 hours
* - `durationLong`: Duration of "remember me" sessions in seconds; defaults to 2 weeks
* - `timeout`: Activity timeout in seconds (integer or false for none); *only* used for normal sessions; defaults to `1800` (half an hour)
+ * - `cookieDomain`: Domain to set the cookie to (this disables the cookie path restriction); defaults to none (default browser behavior)
* - `cookieName`: Name to use for the session cookie; defaults to `kirby_session`
* - `gcInterval`: How often should the garbage collector be run?; integer or `false` for never; defaults to `100`
*/
@@ -38,6 +39,7 @@ class AutoSession
'durationNormal' => 7200,
'durationLong' => 1209600,
'timeout' => 1800,
+ 'cookieDomain' => null,
'cookieName' => 'kirby_session',
'gcInterval' => 100,
...$options
@@ -45,8 +47,9 @@ class AutoSession
// create an internal instance of the low-level Sessions class
$this->sessions = new Sessions($store, [
- 'cookieName' => $this->options['cookieName'],
- 'gcInterval' => $this->options['gcInterval']
+ 'cookieDomain' => $this->options['cookieDomain'],
+ 'cookieName' => $this->options['cookieName'],
+ 'gcInterval' => $this->options['gcInterval']
]);
}
diff --git a/public/kirby/src/Session/Session.php b/public/kirby/src/Session/Session.php
index 2998876..463bf06 100644
--- a/public/kirby/src/Session/Session.php
+++ b/public/kirby/src/Session/Session.php
@@ -465,9 +465,12 @@ class Session
// (re)transmit session token
if ($this->mode === 'cookie') {
+ $cookieDomain = $this->sessions->cookieDomain();
+
Cookie::set($this->sessions->cookieName(), $this->token(), [
'lifetime' => $this->tokenExpiry,
- 'path' => Url::index(['host' => null, 'trailingSlash' => true]),
+ 'path' => $cookieDomain ? '/' : Url::index(['host' => null, 'trailingSlash' => true]),
+ 'domain' => $cookieDomain,
'secure' => Url::scheme() === 'https',
'httpOnly' => true,
'sameSite' => 'Lax'
diff --git a/public/kirby/src/Session/Sessions.php b/public/kirby/src/Session/Sessions.php
index afc5569..3c639eb 100644
--- a/public/kirby/src/Session/Sessions.php
+++ b/public/kirby/src/Session/Sessions.php
@@ -23,6 +23,7 @@ class Sessions
{
protected SessionStore $store;
protected string $mode;
+ protected string|null $cookieDomain;
protected string $cookieName;
protected array $cache = [];
@@ -33,6 +34,7 @@ class Sessions
* @param \Kirby\Session\SessionStore|string $store SessionStore object or a path to the storage directory (uses the FileSessionStore)
* @param array $options Optional additional options:
* - `mode`: Default token transmission mode (cookie, header or manual); defaults to `cookie`
+ * - `cookieDomain`: Domain to set the cookie to (this disables the cookie path restriction); defaults to none (default browser behavior)
* - `cookieName`: Name to use for the session cookie; defaults to `kirby_session`
* - `gcInterval`: How often should the garbage collector be run?; integer or `false` for never; defaults to `100`
*/
@@ -45,9 +47,10 @@ class Sessions
default => new FileSessionStore($store),
};
- $this->mode = $options['mode'] ?? 'cookie';
- $this->cookieName = $options['cookieName'] ?? 'kirby_session';
- $gcInterval = $options['gcInterval'] ?? 100;
+ $this->mode = $options['mode'] ?? 'cookie';
+ $this->cookieDomain = $options['cookieDomain'] ?? null;
+ $this->cookieName = $options['cookieName'] ?? 'kirby_session';
+ $gcInterval = $options['gcInterval'] ?? 100;
// validate options
if (in_array($this->mode, ['cookie', 'header', 'manual'], true) === false) {
@@ -194,6 +197,14 @@ class Sessions
return $this->store;
}
+ /**
+ * Getter for the cookie domain
+ */
+ public function cookieDomain(): string|null
+ {
+ return $this->cookieDomain;
+ }
+
/**
* Getter for the cookie name
*/
diff --git a/public/kirby/src/Template/Snippet.php b/public/kirby/src/Template/Snippet.php
index 4884e66..c805189 100644
--- a/public/kirby/src/Template/Snippet.php
+++ b/public/kirby/src/Template/Snippet.php
@@ -37,24 +37,12 @@ class Snippet extends Tpl
*/
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
*/
@@ -73,11 +61,17 @@ class Snippet extends Tpl
/**
* Creates a new snippet
+ *
+ * @param string|null $file Full path to the PHP file of the snippet;
+ * can be `null` for "dummy" snippets
+ * that don't exist
+ * @param array $data Associative array with variables that
+ * will be set inside the snippet
*/
- public function __construct(string|null $file, array $data = [])
- {
- $this->file = $file;
- $this->data = $data;
+ public function __construct(
+ protected string|null $file,
+ protected array $data = []
+ ) {
}
/**
@@ -158,9 +152,9 @@ class Snippet extends Tpl
array $data = [],
bool $slots = false
): static|string {
- // instead of returning empty string when `$name` is null
- // allow rest of code to run, otherwise the wrong snippet would be closed
- // and potential issues for nested snippets may occur
+ // instead of returning empty string when `$name` is null,
+ // allow rest of code to run, otherwise the wrong snippet would
+ // be closed and potential issues for nested snippets may occur
$file = $name !== null ? static::file($name) : null;
// for snippets with slots, make sure to open a new
@@ -171,7 +165,8 @@ class Snippet extends Tpl
// for snippets without slots, directly load and return
// the snippet's template file
- return static::load($file, static::scope($data));
+ $data = static::scope($data);
+ return static::load($file, $data);
}
/**
@@ -244,10 +239,14 @@ class Snippet extends Tpl
$this->slots[$slotName] = new Slot($slotName, $slotContent);
}
- // custom data overrides for the data that was passed to the snippet instance
+ // custom data overrides the data from the controller
+ // as well as the data passed to the Snippet instance
$data = array_replace_recursive($this->data, $data);
- return static::load($this->file, static::scope($data, $this->slots()));
+ return static::load(
+ file: $this->file,
+ data: static::scope($data, $this->slots())
+ );
}
/**
diff --git a/public/kirby/src/Toolkit/Collection.php b/public/kirby/src/Toolkit/Collection.php
index e7ccbf8..fe6e3b7 100644
--- a/public/kirby/src/Toolkit/Collection.php
+++ b/public/kirby/src/Toolkit/Collection.php
@@ -553,7 +553,7 @@ class Collection extends Iterator implements Stringable
}
}
- return new self($groups);
+ return new self($groups, !$caseInsensitive);
}
throw new Exception(
@@ -625,6 +625,17 @@ class Collection extends Iterator implements Stringable
return $this->count() % 2 !== 0;
}
+ /**
+ * Joins the collection elements into a string,
+ * optionally using a Closure to transform the elements
+ */
+ public function join(
+ string $separator = ', ',
+ Closure|null $as = null
+ ): string {
+ return implode($separator, $this->toArray($as));
+ }
+
/**
* Returns the last element
*
diff --git a/public/kirby/src/Toolkit/Str.php b/public/kirby/src/Toolkit/Str.php
index 863c62e..102f652 100644
--- a/public/kirby/src/Toolkit/Str.php
+++ b/public/kirby/src/Toolkit/Str.php
@@ -477,7 +477,11 @@ class Str
if ($strip === true) {
// ensure that opening tags are preceded by a space, so that
// when tags are skipped we can be sure that words stay separate
- $string = preg_replace('#\s*<([^\/])#', ' <${1}', $string);
+ // but only if there's a word character directly before it
+ $string = preg_replace('#(\w)<([^/][^>]*)>#', '${1} <${2}>', $string);
+
+ // add space after closing tag if there's a word character directly after it
+ $string = preg_replace('#([^>]+)>(\w)#', '${1}> ${2}', $string);
// in strip mode, we always return plain text
$string = strip_tags($string);
diff --git a/public/kirby/src/Toolkit/V.php b/public/kirby/src/Toolkit/V.php
index b51c929..35e01bb 100644
--- a/public/kirby/src/Toolkit/V.php
+++ b/public/kirby/src/Toolkit/V.php
@@ -638,7 +638,7 @@ V::$validators = [
/**
* Checks for a valid Uuid, optionally for specific model type
*/
- 'uuid' => function (string $value, string|null $type = null): bool {
+ 'uuid' => function (string $value, string|array|null $type = null): bool {
return Uuid::is($value, $type);
}
];
diff --git a/public/kirby/src/Uuid/FileUuid.php b/public/kirby/src/Uuid/FileUuid.php
index 2bd2075..61949ac 100644
--- a/public/kirby/src/Uuid/FileUuid.php
+++ b/public/kirby/src/Uuid/FileUuid.php
@@ -88,7 +88,7 @@ class FileUuid extends ModelUuid
/**
* Returns permalink url
*/
- public function url(): string
+ public function toPermalink(): string
{
// make sure UUID is cached because the permalink
// route only looks up UUIDs from cache
@@ -98,4 +98,12 @@ class FileUuid extends ModelUuid
return App::instance()->url() . '/@/' . static::TYPE . '/' . $this->id();
}
+
+ /**
+ * @deprecated 5.1.0 Use `::toPermalink()` instead
+ */
+ public function url(): string
+ {
+ return $this->toPermalink();
+ }
}
diff --git a/public/kirby/src/Uuid/HasUuids.php b/public/kirby/src/Uuid/HasUuids.php
index f55b269..8e55e4d 100644
--- a/public/kirby/src/Uuid/HasUuids.php
+++ b/public/kirby/src/Uuid/HasUuids.php
@@ -19,7 +19,7 @@ trait HasUuids
*/
protected function findByUuid(
string $uuid,
- string|null $scheme = null
+ string|array|null $scheme = null
): Identifiable|null {
// handle UUID shortcuts with a leading @
if ($scheme !== null && str_starts_with($uuid, '@') === true) {
diff --git a/public/kirby/src/Uuid/PageUuid.php b/public/kirby/src/Uuid/PageUuid.php
index c492338..17421ed 100644
--- a/public/kirby/src/Uuid/PageUuid.php
+++ b/public/kirby/src/Uuid/PageUuid.php
@@ -58,7 +58,7 @@ class PageUuid extends ModelUuid
/**
* Returns permalink url
*/
- public function url(): string
+ public function toPermalink(): string
{
// make sure UUID is cached because the permalink
// route only looks up UUIDs from cache
@@ -70,9 +70,17 @@ class PageUuid extends ModelUuid
$url = $kirby->url();
if ($language = $kirby->language('current')) {
- $url .= '/' . $language->code();
+ $url = $language->url();
}
return $url . '/@/' . static::TYPE . '/' . $this->id();
}
+
+ /**
+ * @deprecated 5.1.0 Use `::toPermalink()` instead
+ */
+ public function url(): string
+ {
+ return $this->toPermalink();
+ }
}
diff --git a/public/kirby/src/Uuid/Uuid.php b/public/kirby/src/Uuid/Uuid.php
index 2ce8685..d3d4d7e 100644
--- a/public/kirby/src/Uuid/Uuid.php
+++ b/public/kirby/src/Uuid/Uuid.php
@@ -13,6 +13,7 @@ use Kirby\Cms\User;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\LogicException;
use Kirby\Exception\NotFoundException;
+use Kirby\Toolkit\A;
use Kirby\Toolkit\Str;
use Stringable;
@@ -282,14 +283,16 @@ abstract class Uuid implements Stringable
*/
final public static function is(
string $string,
- string|null $type = null
+ string|array|null $type = null
): bool {
// always return false when UUIDs have been disabled
if (Uuids::enabled() === false) {
return false;
}
- $type ??= implode('|', Uri::$schemes);
+ // use all available schemes by default
+ $type ??= Uri::$schemes;
+ $type = implode('|', A::wrap($type));
$pattern = sprintf('!^(%s)://(.*)!', $type);
if (preg_match($pattern, $string, $matches) !== 1) {
@@ -409,6 +412,32 @@ abstract class Uuid implements Stringable
return $this->uri->toString();
}
+ /**
+ * Returns the URL of the model, including the query and fragment
+ * @since 5.1.0
+ */
+ public function toUrl(): string|null
+ {
+ $model = $this->model();
+
+ if ($model === null) {
+ return null;
+ }
+
+ if (method_exists($model, 'url') === false) {
+ return null;
+ }
+
+ $url = $model->url();
+ $url .= $this->uri->query->toString(true);
+
+ if ($this->uri->hasFragment() === true) {
+ $url .= '#' . $this->uri->fragment();
+ }
+
+ return $url;
+ }
+
/**
* Returns value to be stored in cache
*/
diff --git a/public/kirby/views/php.php b/public/kirby/views/php.php
index 3eefa03..46db0c6 100644
--- a/public/kirby/views/php.php
+++ b/public/kirby/views/php.php
@@ -5,7 +5,7 @@
Advice for developers and administrators:
- Change the PHP version to one supported by your version of Kirby
+ Change the PHP version to one supported by your version of Kirby
diff --git a/public/media/index.html b/public/media/index.html
deleted file mode 100644
index e69de29..0000000