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)#', ' ${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